VS Code extension: `resolveShellPath` uses `spawnSync` and blocks the extension host for ~1s at startup
Follow-up to #67948, which was auto-closed as stale and locked. The issue is still present in 2.1.300.
Summary
On startup, the Claude Code VS Code extension resolves the user's shell PATH by running the login shell synchronously (spawnSync). While the shell runs, the entire extension host is blocked, so every other extension's activation is delayed. With a typical oh-my-zsh + nvm setup this takes ~0.9–1.0s per window. With several restored windows, each one pays the cost.
Environment
- Extension:
anthropic.claude-code2.1.300 (darwin-arm64) - VS Code: 1.142.0 (6d62b1d5dace93a33b3a6d3eca1317bd5e15bcb1), arm64
- OS: macOS (Darwin 27.0.0), Apple M4 Max
- Shell: zsh + oh-my-zsh, nvm plugin with
autoload yes claudebinary installed via Homebrew (/opt/homebrew/bin/claude)
Evidence
CPU profile of the extension host (code --prof-startup, small workspace). From ~1.13s to ~2.04s after extension host start, the extension host is at 100% "busy" with a single stack:
spawnSync node:internal/child_process
spawnSync node:child_process
nF extension.js
zX extension.js
resolveShellPath extension.js
resolveClaudeBinary extension.js
getClaudeBinary extension.js
refreshCliAuthStatus extension.js
getAuthStatus extension.js
processRequest extension.js
Measured block durations:
| Shell config | zsh -lic time (clean env) | Block in profile |
|---|---|---|
| oh-my-zsh + nvm (loaded twice) | 0.86s | ~1000ms |
| oh-my-zsh + nvm (loaded once) | ~0.48s warm / ~0.85s cold | ~910ms |
The command run (from resolveShellPath):
zX(shell, ["-lic", `printf '${X}'; command printenv PATH; printf '${X}'`], { timeout: 1e4 })
zX → nF → child_process.spawnSync. The result is cached in shellPathCache, but only in memory, so every window or startup pays the cost again.
The startup performance report (Developer: Startup Performance) shows the same gap: no other extension loads or activates during this time.
Steps to reproduce
- Use a shell whose interactive login startup takes noticeable time (e.g. oh-my-zsh with the nvm plugin;
time zsh -lic exit≈ 0.5–1s). - Quit VS Code and start it with
code --prof-startup <folder>. - Open the extension host
.cpuprofileand look at ~1–2s: a longspawnSyncfromresolveShellPath.
Expected
Resolving the CLI path should not block the shared extension host. Other extensions should activate while the shell runs.
Suggested fixes (any one would help)
- Use async
execFile/spawnand start resolution early inactivate()as a promise; await it where the binary is needed. - Check known install locations first (
/opt/homebrew/bin/claude,/usr/local/bin/claude,~/.local/bin/claude,~/.claude/local/claude) and fall back to the shell only if none exist. - Use the
PATHalready present in the extension host. VS Code resolves the shell environment itself at startup when launched from the Dock or Finder. - Persist the resolved
PATHinglobalState(keyed by shell) and refresh it asynchronously in the background. - Lower the 10s timeout to bound the worst case.
Impact
- Delays every extension's activation by the user's shell startup time (here ~1s), in every window.
- Shows up as "extension host unresponsive" when combined with other startup load.
- Users with heavier shell configs (nvm, conda, pyenv, asdf) are affected most.