VS Code extension: synchronous shell launch in resolveShellPath blocks event loop, causing extension host to be marked unresponsive
Summary
The Claude Code VS Code extension (v2.1.175) makes a synchronous shell invocation during activation to resolve the user's PATH. If shell startup is slow (e.g. Oh My Zsh, brew shellenv, nvm, etc.), this blocks the Node.js event loop long enough for VS Code to mark the extension host as unresponsive — killing and restarting it in an infinite loop. The extension never successfully initialises.
Behaviour
Every session follows this pattern:
- Extension activates,
AuthManagerinitialised, MCP server starts, OAuth tokens found - Webview sends
{"type":"init"}request - No response is ever sent — the extension host is unresponsive
- VS Code kills the extension host (~90 seconds after marking it unresponsive) and restarts it
- Repeat indefinitely
Observed in VS Code renderer log every session:
Extension host (LocalProcess pid: XXXXX) is unresponsive.
Root Cause
resolveClaudeBinary() calls Gs.resolveShellPath() synchronously to build the PATH environment for child processes:
resolveClaudeBinary() {
let t = kd(Gs.resolveShellPath(this.output)) // ← synchronous, blocks event loop
// ...
}
resolveShellPath runs the user's login interactive shell to capture PATH:
static resolveShellPath(e) {
let t = ce.env.shell // e.g. /bin/zsh
// ...
let { stdout: n } = va(t, ["-lic", `printf '${i}'; command printenv PATH; printf '${i}'`], { timeout: 1e4 })
// va is a synchronous exec (execa.sync / spawnSync)
}
The result is cached in Gs.shellPathCache per process, but the cache is lost on every extension host restart. Since becoming unresponsive triggers a restart, this blocks again each cycle.
Environment
- OS: macOS 15.5 (Darwin 25.5.0, Apple Silicon)
- VS Code: 1.124.0
- Extension:
anthropic.claude-code-2.1.175-darwin-arm64 - Shell:
/bin/zshwith Oh My Zsh — startup time ~1.5 s in terminal, likely longer in VS Code's minimal launch environment
Workaround
Adding a non-TTY short-circuit to ~/.zprofile and ~/.zshrc reduces the shell startup from ~1.5 s to ~27 ms for the extension's PATH probe, which keeps the block below VS Code's unresponsive threshold:
# ~/.zprofile — add before brew shellenv
[[ -t 1 ]] || { export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:$HOME/.local/bin:/usr/local/bin:$PATH"; return; }
# ~/.zshrc — add before Oh My Zsh
[[ -t 1 ]] || return
This works because the extension pipes stdout (so [[ -t 1 ]] is false), whereas real terminal sessions have a TTY attached.
Suggested Fix
One or more of:
- Make
resolveShellPathasync — usechild_process.spawnwith a callback / Promise instead of a synchronous exec. The resolved PATH could be cached before the firstinitrequest is processed. - Persist the shell PATH cache to disk — so it survives extension host restarts and the blocking launch only happens once ever, not once per restart cycle.
- Reduce the shell invocation flags — use
-lc(login, non-interactive) instead of-lic(login, interactive) to avoid sourcing~/.zshrc, which is the main source of latency for many users.