Skip to content

fix: make code <file> work in serve-web terminals#519

Merged
venkatamutyala merged 5 commits into
mainfrom
fix/code-cli-terminal-wrapper
Jul 11, 2026
Merged

fix: make code <file> work in serve-web terminals#519
venkatamutyala merged 5 commits into
mainfrom
fix/code-cli-terminal-wrapper

Conversation

@venkatamutyala

@venkatamutyala venkatamutyala commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Problem

code <file> from the integrated terminal fails two ways in this serve-web environment:

  1. Crash. code resolves to the desktop Electron binary /usr/bin/code, which needs an X display and dies with Missing X server or $DISPLAY.
  2. Silent no-op in tmux. A tmux server started in an earlier session freezes $VSCODE_IPC_HOOK_CLI at an old window's socket. That socket still accepts connections, so code "succeeds" but opens the file in a window you can no longer see.

Fix

A wrapper at $HOME/.local/bin/code (first on the interactive PATH) that forwards to the serve-web remote-cli shim. It:

  1. Resolves the newest shim (ls -t, survives VS Code updates; -x guard → clear error instead of opaque exec failure).
  2. Routes to the newest live window socket. VS Code makes one socket per window/connection under /tmp; the newest live one is your current window. A socat connect probe (bounded by timeout 2 so code can never hang) skips dead sockets left by restarts; if nothing is reachable the inherited value passes through so socket-less commands still work.
  3. Passes tunnel/serve-web through to /usr/bin/code — these are full-CLI subcommands the remote-cli shim doesn't implement (it would treat tunnel as a filename). Uses the absolute path to avoid recursing into the wrapper.

socat is added to the image for the probe. Only affects interactive shells — build-time code --install-extension runs under /bin/sh (no ~/.local/bin on PATH) and still uses /usr/bin/code.

Known limitation

With multiple windows open at once, "newest socket" may not be the exact window you typed in — serve-web exposes no API to map a socket to a window (--status is unsupported in browsers; every live socket answers a connect). This is the best available heuristic and matches community practice (vinnie/tmux, chvolkmann/code-connect); it is correct for the common single-window and stale-tmux cases. Documented inline in the wrapper.

Review & validation

Reviewed by four independent experts (POSIX sh/dash, Linux/sockets, VS Code serve-web internals, Docker build) — no blocking issues; their fixes (executability guard, probe timeout, tunnel/serve-web passthrough) are folded in.

Validated against a full devcontainer build of this image, exercised via docker exec:

  • Wrapper in the built image is byte-identical to the reviewed script; sh -n clean; code resolves to it via .bashrc.
  • code tunnel --help / code serve-web --help → real tunnel/serve-web help (exit 0).
  • No serve-web session → friendly "no usable shim" error (exit 1); never hits the GUI crash path.
  • Socket selection (planted stub sockets): skips a dead-but-newest socket, picks the newest live one, overrides a stale inherited VSCODE_IPC_HOOK_CLI (the tmux fix), and falls through without hanging when nothing is live.

🤖 Generated with Claude Code

The apt `code` package installs the desktop (Electron) binary at
/usr/bin/code. In our headless serve-web/tunnel sessions there is no X
display, so running `code <file>` from the integrated terminal crashes
with "Missing X server or $DISPLAY" (plus a dbus warning).

Add a small wrapper at $HOME/.local/bin/code (already first on the
interactive PATH via .zshrc/.bashrc) that forwards to the VS Code
serve-web/remote-cli shim, which opens files in the running window via
$VSCODE_IPC_HOOK_CLI. It resolves the newest shim so it survives VS Code
updates, and falls back with a clear message if no session is running.

Only affects interactive shells; build-time `code --install-extension`
still resolves to /usr/bin/code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
venkatamutyala and others added 2 commits July 10, 2026 15:21
Extend the `code` wrapper to handle a stale $VSCODE_IPC_HOOK_CLI. The
value is captured when the shell starts, so after a VS Code reconnect
(container/server restart) it can name a socket that no longer exists,
and `code <file>` fails.

When — and only when — the referenced socket is gone, fall back to the
most recently created /tmp/vscode-ipc-*.sock. A still-present socket is
left untouched, so routing stays correct when multiple windows (each
with its own socket) are connected. Dependency-free and no double-exec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Post-review hardening from a shell audit: `ls -t` can surface a shim
path that lacks the execute bit, in which case `exec` fails with an
opaque "exec: Permission denied". Add a `-x` check so that case takes
the existing friendly "no usable shim" branch instead.

Deliberately not adding a "fail if no live socket" guard: socket-less
subcommands (code --version/--list-extensions/--help/tunnel) must still
reach the shim, so socket presence is left for the shim to require.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
venkatamutyala and others added 2 commits July 11, 2026 06:11
A long-lived shell captures $VSCODE_IPC_HOOK_CLI once. A tmux server that
outlives a VS Code reconnect keeps handing every pane the socket of an
OLD window. That socket usually still accepts connections, so
`code <file>` "succeeds" but opens the file in a window the user can no
longer see — the reported "code doesn't work in tmux" symptom. The prior
fallback only triggered when the socket file was missing, so it didn't
help here (the stale file still exists and connects).

Since VS Code creates one socket per window/connection under /tmp, the
newest is the current window. Select the newest socket that still accepts
a connection instead of trusting the inherited value. A best-effort socat
connect probe skips sockets whose window has gone away; if socat is
missing the newest existing socket is used. When no socket is reachable
the inherited value is left as-is so socket-less subcommands (code
--version, --list-extensions, tunnel) keep working. Add socat to the
image so the probe is deterministic.

Verified end to end: with the stale tmux socket forced into the
environment, the wrapper selects the newest live socket and the file
opens in the active window.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a passthrough so `code tunnel` and `code serve-web` (full-CLI
subcommands the remote-cli shim does not implement — it would treat
"tunnel" as a filename) exec the real /usr/bin/code, which dispatches
them to its bundled CLI and runs headless. Everything else keeps routing
through the shim.

Bound the socket liveness probe with `timeout 2` so a pathological
connect() can never wedge `code`. Document the single-window limitation
of the "newest socket" heuristic inline.

Validated in a full `devcontainer build` of this image via docker exec:
tunnel/serve-web show their real help; socket selection skips a
dead-but-newest socket, picks the newest live one, overrides a stale
inherited VSCODE_IPC_HOOK_CLI (the tmux case), and falls through without
hanging when nothing is live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added enhancement New feature or request minor labels Jul 11, 2026
@venkatamutyala venkatamutyala merged commit a0769ed into main Jul 11, 2026
2 checks passed
@venkatamutyala venkatamutyala deleted the fix/code-cli-terminal-wrapper branch July 11, 2026 07:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants