Skip to content

app-server.mjs:188 — spawn("codex") fails on Windows without shell:true #337

@bsouthers

Description

@bsouthers

Summary

On Windows, the codex-companion plugin fails to start the codex app-server because
child_process.spawn("codex", ...) at scripts/lib/app-server.mjs:188 is invoked
without { shell: true }. Node on Windows does not auto-resolve PATHEXT (.cmd,
.ps1) extensions for spawn unless shell: true is set, so the npm-installed
codex.cmd wrapper is invisible. Result: spawn codex ENOENT.

This causes two user-visible failure modes downstream:

  1. codex-audit / direct review invocations fail fast with spawn codex ENOENT.
  2. codex:rescue (task mode) blocks the foreground for an extended period
    (observed ~50 minutes) and /codex:status blocks ~2 hours before erroring,
    apparently retrying the failed spawn without surfacing the underlying ENOENT.

Environment

  • OS: Windows 11 Home (10.0.26200)
  • Node: v22.14.0
  • npm: 10.9.2
  • codex-cli: 0.131.0 (installed globally via npm i -g)
  • Claude Code with openai-codex plugin (latest as of 2026-05-19)
  • Shell: PowerShell 5.1 (main session); subagent dispatches use Bash via Git for Windows

Reproduction

From PowerShell, codex --version succeeds:

PS> (Get-Command codex).Source
C:\Users\<user>\AppData\Roaming\npm\codex.ps1
PS> codex --version
codex-cli 0.131.0

But Node spawn without shell fails:

PS> node -e 'const{spawn}=require("child_process");const p=spawn("codex",["--version"]);p.on("error",e=>console.log(e.code));'
ENOENT

With shell: true, it succeeds:

PS> node -e 'const{spawn}=require("child_process");const p=spawn("codex",["--version"],{shell:true});p.stdout.on("data",d=>process.stdout.write(d));'
codex-cli 0.131.0

Root cause

scripts/lib/app-server.mjs:188:

this.proc = spawn("codex", ["app-server"], {
  cwd: this.cwd,
  env: this.options.env,
  stdio: ["pipe", "pipe", "pipe"]
});

The plugin already uses the correct pattern elsewhere — scripts/lib/process.mjs:11
sets shell: process.platform === "win32" for its runCommand spawns. The
app-server spawn was missed.

Proposed fix

One-line addition:

     this.proc = spawn("codex", ["app-server"], {
       cwd: this.cwd,
       env: this.options.env,
-      stdio: ["pipe", "pipe", "pipe"]
+      stdio: ["pipe", "pipe", "pipe"],
+      shell: process.platform === "win32"
     });

Verified locally: applying this patch unblocks the codex-rescue dispatch path
(smoke test completed end-to-end in 53s vs. indefinitely hanging before).

Why this matters

The hang failure mode is particularly costly because it masquerades as Codex
"still working" rather than a config error — users wait for hours before
realizing the task is wedged. A clean ENOENT surface would be better than the
current silent-hang behavior.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions