DEV Community

Gaurav Batra
Gaurav Batra

Posted on

VHS 0.12.0 exits successfully but creates no GIF or video: a canceled context is why

Last week I set out to learn demo-video production, starting with the obvious tool for a CLI developer: VHS. My very first render printed Creating v1.mp4... and exited 0. There was no MP4. My first demo video was going great.

The tape's commands had executed. Repeating the run with GIF and WebM outputs produced the same result. Output frames/ worked, which narrowed the problem to encoding rather than terminal capture.

This affects VHS v0.12.0, the latest release as of September 23, 2026. It is tracked upstream in issue #787. The issue has reproductions on Linux, macOS, and Windows.

A small reproduction

With v0.12.0 installed, try a tape like this:

Output demo.mp4
Set Width 600
Set Height 300
Type "echo hello"
Enter
Sleep 2s
Enter fullscreen mode Exit fullscreen mode

On affected builds, vhs demo.tape reaches the Creating demo.mp4... message and exits 0, but demo.mp4 is absent. Switching the output to .gif or .webm does not help. A frames-only output can still be written.

What happens in the code

In evaluator.go, Evaluate creates a context for recording and assigns it back to ctx:

ctx, cancel := context.WithCancel(ctx)
ch := v.Record(ctx)
Enter fullscreen mode Exit fullscreen mode

At the end of the tape, teardown() calls cancel() to stop recording. The next call is v.Render(ctx). That is the same context, now canceled.

Render builds the GIF, MP4, and WebM commands using that context. Go's exec.CommandContext will not start ffmpeg with an already canceled context. Then Render logs the command output but returns nil. When the process never starts, that output is empty, so VHS can print a blank line and report success without creating a file.

What worked for me

I built VHS from the v0.12.0 source with a small local change: use a separate recordingCtx for Record, cancel only that context during teardown, and pass the still-live parent ctx to Render. The same tape then produced a valid MP4. The frames-only result before the patch helped confirm that recording itself was healthy.

The relevant change is small:

 // Begin recording frames as we are now in a recording state.
-ctx, cancel := context.WithCancel(ctx)
-ch := v.Record(ctx)
+recordingCtx, cancel := context.WithCancel(ctx)
+ch := v.Record(recordingCtx)
Enter fullscreen mode Exit fullscreen mode

Later, teardown() cancels only recordingCtx, while Render receives the live parent ctx.

I'm not proposing this as the fix — that conversation is happening in #788–791. This is just what unblocked me.

If you need an unpatched release today, v0.11.0 is the practical rollback reported in the upstream issue. Check which executable your shell actually runs with command -v vhs and vhs --version; an older binary on disk will not help if a v0.12.0 installation appears earlier on PATH.

Upstream status

The bug is documented in #787, and fix proposals are open in #788, #789, #790, and #791. The separate recording context addresses the canceled render. Returning encoder errors to the caller would also prevent future failures from looking like successful runs.

Until a fix is merged and released, treat exit code 0 from v0.12.0 as insufficient proof that an output file was created. The exit code tells you the process thinks it finished, not that the artifact exists. Check the file.

This was day 1 of learning demo videos in public.

Next: making the video this bug almost prevented.

Top comments (0)