Skip to content

fix: resolve ENOENT for .cmd shims on Windows - #13

Merged
dkundel-openai merged 1 commit into
openai:mainfrom
omid-method:fix/windows-spawnSync-shell
Mar 31, 2026
Merged

dkundel-openai merged 1 commit into
openai:mainfrom
omid-method:fix/windows-spawnSync-shell

Conversation

@omid-method

Copy link
Copy Markdown

Summary

  • Adds shell: process.platform === "win32" to spawnSync in runCommand(), fixing binary detection (codex, npm, etc.) on Windows

Problem

On Windows, npm installs global CLI tools as .cmd shims (e.g., codex.cmd). Node.js spawnSync() without shell: true calls CreateProcess, which only resolves .exe files — it cannot find .cmd or .bat files, even when they are on PATH.

This causes binaryAvailable("codex", ...) to return { available: false, detail: "not found" } on Windows, even with a correctly installed and PATH-accessible Codex CLI. The same applies to npm detection.

Before (on Windows)

codex: { available: false, detail: "not found" }
npm:   { available: false, detail: "not found" }

After

codex: { available: true, detail: "codex-cli 0.117.0; advanced runtime available" }
npm:   { available: true, detail: "11.6.3" }

Root cause

spawnSync("codex", ["--version"]) → Windows CreateProcess → searches for codex.exe only → ENOENT.

Adding shell: true routes through cmd.exe, which resolves commands using PATHEXT (.CMD, .BAT, etc.), matching how the Windows shell resolves commands.

Why this is safe

  • The condition process.platform === "win32" evaluates to false on macOS/Linux — zero behavior change on non-Windows platforms
  • shell: true is the standard Node.js pattern for cross-platform command resolution (used by cross-spawn, execa, and npm itself)
  • terminateProcessTree already works on Windows because taskkill is a native .exe, not a .cmd shim — so it is unaffected by this change

Test plan

  • Verified on Windows 11 + Node.js v22.21.0 + Codex CLI 0.117.0
  • /codex:setup reports all components as available after the fix
  • Verify no regression on macOS/Linux (condition is false, no code path change)

On Windows, Node.js `spawnSync` without `shell: true` uses
`CreateProcess`, which only resolves `.exe` files. npm installs global
tools (like `codex`) as `.cmd` shims, so `spawnSync("codex", ...)`
returns ENOENT even when codex is correctly installed and on PATH.

Adding `shell: process.platform === "win32"` routes through `cmd.exe`
on Windows, which properly resolves `.cmd`, `.bat`, and PATHEXT entries.
No behavior change on macOS/Linux since the condition evaluates to false.
@omid-method
omid-method requested a review from a team March 30, 2026 21:24
@dkundel-openai

Copy link
Copy Markdown
Collaborator

@codex review

@dkundel-openai
dkundel-openai merged commit cf6f851 into openai:main Mar 31, 2026
0xZOne added a commit to 0xZOne/codex-plugin-cc that referenced this pull request Mar 31, 2026
…lity

On Windows, `codex` is installed as an npm `.cmd` shim. Node's
`child_process.spawn()` cannot resolve `.cmd` files without
`shell: true`, causing `ENOENT` when `SpawnedCodexAppServerClient`
tries to start `codex app-server`.

This is the same class of bug fixed in openai#13 for `spawnSync` in
`process.mjs`, but the async `spawn()` call in `app-server.mjs`
was missed.
tgkim-openerd referenced this pull request in opnd-io/opnd-codex-plugin May 27, 2026
…-4.5, openai#310)

Upstream codex CLI's JSONL parser assumes a UTF-8 byte stream on its
stdio. On a host where LANG / LC_ALL points at Big5 / GBK / EUC-* /
plain `C`, codex panics with `invalid utf-8 sequence` mid-run — the
most cited reproducer is zh-TW (`LANG=zh_TW.Big5`), but every
non-UTF-8 locale is affected.

The real fix lives in the codex CLI itself. This PR adds the
plugin-side mitigation: when buildPluginCodexEnv composes the spawned
codex child's env, it walks the POSIX locale precedence ladder
(LC_ALL → LC_CTYPE → LANG) and, if the effective locale is not
UTF-8, overrides BOTH `LC_ALL` and `LANG` for the child only — the
user's own shell env is untouched. A one-shot stderr notice surfaces
on the first override per process so the user is not surprised by a
silent locale rewrite affecting codex output formatting.

What lands in this PR
---------------------

- plugins/codex/scripts/lib/app-server.mjs
  - New `applyUtf8LocaleOverride(target, base)` pure function. POSIX
    precedence via `effectiveLocaleIsUtf8(env)` so LC_ALL=C with
    LANG=en_US.UTF-8 correctly triggers the override (the EFFECTIVE
    locale codex sees is C, not en_US.UTF-8).
  - `looksUtf8Locale(value)` recognizer for `.utf-?8`, `C.UTF-8`,
    `POSIX.UTF-8` (case-insensitive, accepts trailing modifiers like
    `@iso14651_t1` and double extensions like `.UTF-8.iso88591`).
  - Override target is platform-aware: `C.UTF-8` on POSIX (glibc
    portable sentinel, no locale data files needed), `en_US.UTF-8` on
    Windows (the codex CLI's Windows runtime layer is friendlier to
    real locale strings than to the synthetic `C.UTF-8`).
  - `CODEX_PLUGIN_PRESERVE_LOCALE=1` opt-out for users who need
    localized codex output (translated messages, region-specific
    formats) at the cost of the openai#310 crash risk.
  - Module-level `localeOverrideNoticeEmitted` latch + first-call
    stderr notice naming both the detected host LANG/LC_ALL and the
    opt-out env var.
  - `__resetAppServerNoticeCache()` test helper exposed alongside
    the existing repo convention (double-underscore prefix — see
    tests/state.test.mjs callers in lib/state.mjs).
  - `buildPluginCodexEnv` reorganized to compose home-isolation +
    locale-override as independent transforms; opt-out of one does
    NOT opt out of the other.

- tests/locale-utf8-mitigation.test.mjs (NEW, 26 cases)
  - applyUtf8LocaleOverride pure-function contract (UTF-8 variants,
    Big5 / GBK / C / unset paths, opt-out, immutability).
  - buildPluginCodexEnv integration (home isolation + locale overlay
    independence, stderr notice firing, second-call dedup).
  - Source-level invariants (function/constant presence, openai#310
    cross-references).
  - Audit-finding regression guards (#1 POSIX precedence including
    empty LC_ALL fall-through to LC_CTYPE, #3 Windows en_US.UTF-8,
    #4 latch-before-write attempted-once design, #5 test-helper
    convention, #6 GFM slug correctness).

- docs/TROUBLESHOOTING.md
  - New section #13 with symptom / cause / mitigation / opt-out /
    verify. The two old "Known investigations" bullets for openai#310 are
    removed from the bottom list since the mitigation now ships.

- docs/MIGRATION_v2.0.md
  - New "v2.1.0 env vars" section row for
    CODEX_PLUGIN_PRESERVE_LOCALE with cross-doc link to the
    TROUBLESHOOTING #13 explainer.

Codex audit findings (6) — all addressed in this commit
-------------------------------------------------------

A pre-commit `codex:codex-rescue` audit on the runtime diff surfaced 6
findings. Every one is fixed inline or explicitly documented as a
deliberate trade-off, each with a dedicated audit-* regression test:

- Finding 1 (HIGH) — POSIX precedence violation: LC_ALL=C with
  LANG=en_US.UTF-8 used to short-circuit on LANG (UTF-8) and skip
  the override, even though the EFFECTIVE locale codex sees is C.
  Now walks the LC_ALL → LC_CTYPE → LANG ladder explicitly.
- Finding 2 (MEDIUM) — broker reuse freezes the locale env at broker
  start. Mid-session LANG / LC_ALL changes need a broker restart.
  Documented as a deliberate trade-off (the realistic case is "user
  fixes locale, then resumes" which already needs a restart).
- Finding 3 (LOW) — Windows uses `en_US.UTF-8` instead of the
  glibc-ism `C.UTF-8` so the override actually takes effect on the
  Windows codex runtime.
- Finding 4 (LOW) — latch flips BEFORE the stderr write to keep the
  "attempted-once" semantic on a broken stderr (otherwise a noisy
  retry loop would emerge). Documented with comment + regression
  test.
- Finding 5 (LOW) — `__resetAppServerNoticeCache` export is
  test-only by convention (double-underscore prefix). No NODE_ENV
  gate because the repo's normal `node --test` entrypoint does not
  set NODE_ENV.
- Finding 6 (LOW) — MIGRATION cross-doc anchor slug corrected to
  match GFM's single-hyphen-at-`+` rule + stripped `openai#310`
  parentheses.

Out of scope for this PR
------------------------

- The upstream codex CLI fix. This PR is the plugin-side workaround
  while the parser issue is open upstream.
- Auto broker restart on mid-session locale change (audit finding
  #2). The current restart-required behavior is documented; revisit
  if a real user hits this case repeatedly.
- A diagnostic helper that surfaces the effective locale codex sees
  via `/codex:setup --json`. Worth a follow-up but not bundled here.

Refs PR-4.5 in
docs/ultraplan/2026-05-16-130611-codex-plugin-cc-implementation-ultraplan.md.
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.

3 participants