Summary
libs/agent-runner/{codex,opencode,openhands}.ts all spawn their respective
agent CLIs (codex, opencode, presumably the openhands binary) using Node's
child_process.spawn() without shell: true, e.g.:
const child = spawn("opencode", args, {
cwd: input.cwd,
env: input.env,
stdio: ["ignore", "pipe", "inherit"],
});
This likely fails on Windows whenever the target CLI is installed the standard
way -- via npm install -g, which creates a .cmd shim rather than a native
.exe.
Why
spawn() without shell: true calls Windows' CreateProcess directly.
CreateProcess cannot execute .cmd/.bat files -- they aren't in native
executable (PE) format and require cmd.exe /c <file> as an intermediary to
run at all. This is an OS-level restriction, not a Node bug or version quirk.
How I found this
While building a test for a separate opencode fix (argv secret exposure), I
tried spawning a fake opencode.cmd stand-in the exact same way the real
runner does. It failed immediately with:
Error: spawn EINVAL
at ChildProcess.spawn (node:internal/child_process:441:11)
reproduced on a real Windows machine (Node v24.16.0), not a POSIX simulation.
Since the real production spawn call uses the identical pattern
(spawn("opencode", args, { ...no shell:true... })), this points to the same
failure happening for real if opencode is npm-installed on Windows.
Impact
If confirmed, the background auto-memory-update hook (libs/hooks/worker.ts)
would silently fail on Windows for any platform whose CLI is npm-installed --
likely all three (codex, opencode, openhands installers all follow the
same spawn pattern). This is a portability/functional bug, not a security
issue -- separate root cause from #, even though
found in the same file.
Suggested fix
The standard solution for "spawn needs to work with .cmd/.bat shims on
Windows, cross-platform, without shell-injection risk" is the cross-spawn
npm package. It detects .cmd/.bat targets and handles the
cmd.exe /c wrapping (with correct argument escaping) automatically,
avoiding the injection risk that comes with a blanket shell: true.
Would apply to all three agent runners identically, since they share the
same spawn pattern.
To verify before fixing
Summary
libs/agent-runner/{codex,opencode,openhands}.tsall spawn their respectiveagent CLIs (
codex,opencode, presumably the openhands binary) using Node'schild_process.spawn()withoutshell: true, e.g.:This likely fails on Windows whenever the target CLI is installed the standard
way -- via
npm install -g, which creates a.cmdshim rather than a native.exe.Why
spawn()withoutshell: truecalls Windows'CreateProcessdirectly.CreateProcesscannot execute.cmd/.batfiles -- they aren't in nativeexecutable (PE) format and require
cmd.exe /c <file>as an intermediary torun at all. This is an OS-level restriction, not a Node bug or version quirk.
How I found this
While building a test for a separate opencode fix (argv secret exposure), I
tried spawning a fake
opencode.cmdstand-in the exact same way the realrunner does. It failed immediately with:
reproduced on a real Windows machine (Node v24.16.0), not a POSIX simulation.
Since the real production spawn call uses the identical pattern
(
spawn("opencode", args, { ...no shell:true... })), this points to the samefailure happening for real if
opencodeis npm-installed on Windows.Impact
If confirmed, the background auto-memory-update hook (
libs/hooks/worker.ts)would silently fail on Windows for any platform whose CLI is npm-installed --
likely all three (
codex,opencode,openhandsinstallers all follow thesame spawn pattern). This is a portability/functional bug, not a security
issue -- separate root cause from #, even though
found in the same file.
Suggested fix
The standard solution for "spawn needs to work with .cmd/.bat shims on
Windows, cross-platform, without shell-injection risk" is the
cross-spawnnpm package. It detects
.cmd/.battargets and handles thecmd.exe /cwrapping (with correct argument escaping) automatically,avoiding the injection risk that comes with a blanket
shell: true.Would apply to all three agent runners identically, since they share the
same spawn pattern.
To verify before fixing
opencode(or
codex) actually reproduces this when triggered viagreplica install+ a real coding session (not just my synthetic.cmdstand-in)..exeinstead of an npm
.cmdshim (would make this moot for that specifictool).