Skip to content

fix: reject nested vhs invocations instead of panicking#768

Open
arimu1 wants to merge 1 commit into
charmbracelet:mainfrom
arimu1:fix/761-nested-render-error
Open

fix: reject nested vhs invocations instead of panicking#768
arimu1 wants to merge 1 commit into
charmbracelet:mainfrom
arimu1:fix/761-nested-render-error

Conversation

@arimu1

@arimu1 arimu1 commented Jul 19, 2026

Copy link
Copy Markdown

Problem

Fixes #761.

Running vhs inside a tape that is itself being recorded (e.g. a tape that types vhs other.tape and hits Enter) starts a second ttyd + browser pair inside the first recording session. The inner instance shares the outer process's environment and races with the outer instance's own ttyd/browser lifecycle (port selection, process teardown, etc.). Depending on timing this crashes the whole program:

panic: write tcp 127.0.0.1:<port>->127.0.0.1:<port>: use of closed network connection

Repro from the issue:

  1. inner.tape: any minimal tape (Output inner.gif, one Type/Enter).
  2. outer.tape: Output outer.gif, Type "vhs inner.tape", Enter, Sleep 10s.
  3. vhs outer.tape → panic.

I reproduced this locally (macOS, Chrome + ttyd 1.7.7) — nested rendering sometimes succeeds and sometimes fails, confirming it's a race rather than a deterministic error, consistent with the reporter's "reproduced twice" note on Windows/WSL.

Why full nested-rendering support is out of scope

Making nested rendering actually work would require isolating ttyd's port allocation, the browser's remote-debugging connection, and process teardown ordering between the outer and inner vhs instances — effectively giving each recording session its own sandboxed environment. That's a much larger change than a bug fix, and the issue reporter explicitly said a graceful error is an acceptable resolution.

Fix

  • VHS.Start now exports VHS_RECORDING=1 into the environment of the shell/ttyd process it drives (tty.go), so any vhs invocation typed into that shell inherits it.

  • Before launching ttyd/the browser, VHS.Start (vhs.go) checks for VHS_RECORDING and, if set, returns immediately with a clear error instead of proceeding:

    vhs is already recording; nested rendering is not supported

This fails fast before any ttyd/browser process is spawned for the inner instance, so there's nothing left to race or panic.

Testing

  • go build ./...
  • go vet ./...
  • go test ./...
  • Added TestStart_NestedRecordingRejected in vhs_test.go, which asserts VHS.Start returns the nested-recording error (and leaves vhs.started false) when VHS_RECORDING is set, without needing ttyd/a browser.
  • Manually reproduced end-to-end with the tapes from the issue (built locally with ttyd 1.7.7 and Chrome installed): the outer tape now renders successfully and the recorded terminal shows the inner vhs invocation printing vhs is already recording; nested rendering is not supported and exiting with a non-zero status, instead of panicking.

Running `vhs` inside a tape that is itself being recorded (e.g. a
tape that types `vhs other.tape`) spins up a second ttyd/browser pair
sharing the outer process's environment. Depending on timing, this
races with the outer instance's own ttyd/browser teardown and can
crash the whole process with `panic: write tcp ...: use of closed
network connection`.

Full nested-rendering support is out of scope, so instead fail fast:
VHS now exports VHS_RECORDING=1 into the shell it drives, and
VHS.Start checks for it before launching ttyd/the browser, returning
a clear "vhs is already recording; nested rendering is not
supported" error rather than letting the inner instance run and
potentially panic.

Fixes charmbracelet#761
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nested vhs render inside a recording panics with 'use of closed network connection'

1 participant