Skip to content

fix: resolve Windows ENOENT when spawning codex app-server#55

Merged
dkundel-openai merged 2 commits intoopenai:mainfrom
zeta987:fix/windows-spawn-enoent-core
Mar 31, 2026
Merged

fix: resolve Windows ENOENT when spawning codex app-server#55
dkundel-openai merged 2 commits intoopenai:mainfrom
zeta987:fix/windows-spawn-enoent-core

Conversation

@zeta987
Copy link
Copy Markdown
Contributor

@zeta987 zeta987 commented Mar 31, 2026

Summary

  • Adds shell: true (Windows-only) and windowsHide: true to the spawn("codex", ...) call in app-server.mjs so Node.js resolves .cmd shims installed by npm.
  • Uses terminateProcessTree in close() to kill the entire process tree on Windows, preventing orphaned node.exe processes caused by shell: true wrapping the child in cmd.exe.
  • Adds windowsHide: true to the existing spawnSync call in process.mjs to suppress transient console window flashes.

Problem

On Windows, npm installs codex as a global package and exposes it through a .cmd shim (codex.cmd). When app-server.mjs calls spawn("codex", ["app-server"]) without shell: true, Node.js attempts to execute codex as a raw binary. This fails immediately:

Error: spawn codex ENOENT
    at ChildProcess._handle.onexit (node:internal/child_process:286:19)

This error breaks /codex:setup and /codex:review on Windows. The spawnSync path in process.mjs was resolved in PR #13, but the asynchronous spawn path in app-server.mjs requires a similar fix.

Solution

plugins/codex/scripts/lib/app-server.mjs (+17/−1)

  • Updates spawn("codex", ["app-server"]) to include shell: process.platform === "win32" and windowsHide: true. The platform guard leaves macOS and Linux behavior unchanged.
  • Adds a Windows-specific branch to close() that calls terminateProcessTree(this.proc.pid) instead of SIGTERM. The shell: true option makes cmd.exe the direct child process; using SIGTERM terminates cmd.exe but leaves the node-based codex process orphaned. The call is wrapped in a try/catch block because it executes inside an unref'd timer during shutdown.

plugins/codex/scripts/lib/process.mjs (+2/−1)

Why not just shell: true?

Using shell: true resolves the ENOENT error but introduces two secondary issues on Windows:

  • Zombie processes: Node.js spawns cmd.exe /d /s /c codex app-server. The existing SIGTERM logic in close() kills cmd.exe, but the node.exe grandchild survives. Testing showed 274+ orphaned node.exe processes accumulating across plugin reloads. terminateProcessTree uses taskkill /PID <pid> /T /F to terminate the entire tree.
  • Console window flash: Every spawn call with shell: true briefly flashes a cmd.exe console window. The windowsHide: true option suppresses this using CREATE_NO_WINDOW.

Testing

Verified on Windows 11 running Claude Code with the codex plugin:

Scenario Before After
/codex:setup spawn codex ENOENT Completes successfully
/codex:review spawn codex ENOENT Completes successfully
Plugin reload (×10) 274 orphaned node.exe 0 orphaned processes
Console window flash Visible cmd.exe popup No visible window
macOS/Linux behavior N/A (unchanged) N/A (unchanged)

Related

Fixes: #32, #46

Related PRs:

On Windows, spawn("codex", ["app-server"]) fails with ENOENT because
Node.js cannot resolve .cmd shims without shell: true. This adds
platform-gated shell and windowsHide options to the app-server spawn
call, and uses terminateProcessTree for proper process tree cleanup
since shell: true wraps the child in cmd.exe.

Without terminateProcessTree, plain SIGTERM only kills cmd.exe and
leaves the actual codex node process orphaned — verified with 274+
zombie node.exe processes accumulating on Windows.

Fixes openai#32
Fixes openai#46

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@zeta987 zeta987 requested a review from a team March 31, 2026 08:27
ChildProcess.killed only reflects whether .kill() was called by this
process — it stays false when the child exits on its own. On Windows,
where PIDs are recycled quickly, the 50 ms timer could fire after
cmd.exe has exited and its PID has been reassigned, causing taskkill
to terminate an unrelated process.

Add an exitCode === null check so the tree-kill path is skipped once
the child has already exited.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@zeta987
Copy link
Copy Markdown
Contributor Author

zeta987 commented Mar 31, 2026

I've added a follow-up commit to guard the delayed terminateProcessTree call against potential PID reuse on Windows.

Since ChildProcess.killed stays false when a child exits on its own, the 50ms timer could theoretically fire after cmd.exe has exited and its PID has been reassigned to an unrelated process.

Adding an exitCode === null check prevents this edge case by ensuring we only run taskkill while the child is still actively running:

if (this.proc && !this.proc.killed && this.proc.exitCode === null) {

Verified on Windows 11; local testing confirms that shutdown behavior remains correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants