Windows: resolve npm shims and shebang CLIs before spawning (no agent CLI is detected at all) - #41
Conversation
On Windows the agent CLIs are never found, so every provider reports
"unavailable" and no turn can start.
Three separate reasons, all in the spawn path:
- libuv does not apply PATHEXT, so spawn("claude") looks for a file
literally named `claude` and never finds `claude.cmd`.
- Since Node's CVE-2024-27980 fix, spawning a `.cmd` without
`shell: true` throws synchronously.
- Windows has no `#!` support, so a node-shebang script (every fake CLI
under server/testing) cannot be executed as itself.
`shell: true` is not an option: the drivers pass raw JSON in argv
(claude's `--mcp-config`), which cmd would mangle.
So resolve the target ourselves. `resolveCliSpawn(cli, args)` does a
PATHEXT-aware `which`, parses npm/pnpm `.cmd` shims down to the `.exe`
or node script they wrap, rewrites `#!node` scripts to `node <script>`,
and only falls back to ComSpec for an unparseable shim — with the
double `^`-escaping that fallback then owns, so a raw-JSON argument
survives byte for byte. Off win32 it is the identity function: POSIX
already resolves PATH and `#!` in the kernel.
Every driver spawn/execFile goes through it: claude (spawn, snapshot,
generateText), codex (spawn, snapshot), acp/core (spawn, snapshot).
The spawn-based suites were skipped on win32 for exactly this reason, so
they are unskipped here; server/testing/setup.ts gets a short retry
around the temp-home cleanup (Windows holds a directory that was a live
process's cwd for a beat after the kill returns) and comms.test.ts
passes SystemRoot through to the child, without which winsock fails to
initialize.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgJeiantdRcZSBrc5sqqCp
|
Independent run of this branch on a real Windows box — Windows 11, Node 22, pnpm 10.33. CI's windows leg already covers the suite, but since the repo is macOS-primary a from-the-machine report seemed worth having. This PR: Same machine, for context: I also went looking for a hole in
Nothing executed in any case. The double-escape is doing what the comment says it does. One thing worth a maintainer's eye rather than a change here: CONTRIBUTING says "Never build command strings for a shell. No |
milind-soni
left a comment
There was a problem hiding this comment.
This remains relevant: current main's server/procs.ts still routes .cmd/.bat shims through cmd.exe, which conflicts with CONTRIBUTING's no-shell rule and leaves ACP/model arguments exposed to cmd metacharacter parsing. The shim-unwrapping and shebang resolution here are worth keeping, but the branch needs a rebase around current procs.ts and the newer Windows/Antigravity paths before it can merge.
Integrate #41: Windows no-shell CLI resolution
|
The core Windows CLI-resolution work has been integrated into |
Integrate the resolver through the centralized process launcher, preserve newer provider lifecycle handling, cover Antigravity, and keep unparseable shims out of cmd.exe.
What's broken
On Windows, no agent CLI is ever detected. Every provider reports
unavailableregardless of what is installed, so no bot can be selected and no turn can start — the app is unusable on the platform.Measured on Windows 10 with Claude Code 2.1.229 and codex-cli 0.144.6 both installed and working in the terminal, against
main(8511f02):Root cause
Three separate problems, all in the spawn path, all Windows-specific:
PATHEXT.spawn("claude")looks for a file named literallyclaudeand walks straight pastclaude.cmd, which is what npm actually installs. (npm writes both: aclaudesh script for Git Bash and aclaude.cmdshim for Windows. Only the latter is runnable here.).cmdcannot be spawned directly any more. Since Node's fix for CVE-2024-27980, spawning a.cmd/.batwithoutshell: truethrows synchronously.#!support. A node-shebang script — every fake CLI underserver/testing/— cannot be executed as itself.shell: trueis not an option here. The drivers pass raw JSON in argv (claude's--mcp-config), and handing that tocmdmangles it.The fix
resolveCliSpawn(cli, args)inserver/env-path.tsworks out how to actually spawn the thing, without a shell:whichoveraugmentedPath(), preferring the PATHEXT hit over an extensionless sibling..cmdshims are parsed down to what they actually run — the.exe, ornode <cli.js>for the_progshim shape (never the shim's own bundlednode.exe).#!...nodescripts becomenode <script>.ComSpec, and that path owns its escaping: arguments are quoted for the CRT parser and then^-escaped twice, because a.cmd's%*hands the line to a second round of cmd parsing. A raw-JSON--mcp-configpayload survives byte for byte (there is a test pinning exactly that).spawnreports its ownENOENTas before.Off win32 it is the identity function — POSIX resolves PATH and
#!in the kernel, so nothing changes there.Every driver spawn/execFile goes through it:
claude(spawn, snapshot, generateText),codex(spawn, snapshot),acp/core(spawn, snapshot).Tests
The spawn-based suites were skipped on win32 for exactly this reason, so they are unskipped here. Two supporting changes were needed to make them actually pass on Windows:
server/testing/setup.tsretries the temp-home cleanup briefly. Windows holds a directory that was a live process's cwd for a beat after the kill returns, andrmSync's ownmaxRetriesdoes not cover anEPERMon the directory itself.server/comms.test.tspassesSystemRootthrough to the spawned server, without which winsock fails to initialize in the child.server/drivers/claude.test.tsstays skipped on win32 in this PR — it additionally needs the permission-broker fix, and is unskipped in #42 (stacked on this one). The other Windows fixes in this series, #43 and #44, are independent of this PR and of each other.How this was tested
pnpm typecheckandpnpm teston Windows 10: 77 passed / 15 skipped, up from 52 passed / 33 skipped onmain. Nothing regressed; the delta is entirely previously-skipped tests now running.resolveCliSpawncovering both real npm shim shapes, the PATHEXT-vs-extensionless preference, the shebang rewrite, the ComSpec fallback with a raw-JSON payload round-tripped throughcmd, and the not-found passthrough. The two shim fixtures are the exact bytes npm writes.macOS behaviour is unchanged by construction —
resolveCliSpawnreturns its input unmodified off win32.🤖 Generated with Claude Code
https://claude.ai/code/session_01HgJeiantdRcZSBrc5sqqCp