openai / openai/codex-plugin-cc

Windows: SHELL env var (Git Bash) breaks taskkill; handleCancel swallows terminateProcessTree exceptions, leaving jobs stuck in running/finalizing

Open
#647 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
33.3k
Forks
2.3k
PR merge metrics
No merged PRs in 30d

Description

Title

Windows: SHELL env var (Git Bash) breaks taskkill invocation; handleCancel swallows no exceptions from terminateProcessTree, leaving jobs stuck in running/finalizing forever

Environment

  • OS: Windows 11 Pro (10.0.26200)
  • Plugin: openai-codex (codex plugin) v1.0.6
  • Claude Code's Bash tool runs via Git Bash on this machine, so process.env.SHELL is set to /bin/bash.exe for the lifetime of any child process spawned from a Bash-tool invocation (confirmed this is not a persistent Windows user/system env var — [Environment]::GetEnvironmentVariable("SHELL", "User"/"Machine") both return null; it's set transiently by the Git Bash shell itself).
  • Not reproducible on macOS/Linux.

Summary

Two independent Windows-only defects compound into the same visible symptom (a rescue/task job that never leaves running, and codex-companion.mjs cancel exiting non-zero with garbled taskkill error output). Neither matches the root cause described in #639 (which is about tracked-jobs.mjs lacking a liveness check for worker self-reported status) — these are in a different code path (lib/process.mjs, lib/app-server.mjs, and the handleCancel handler in codex-companion.mjs).

Bug 1 — shell: option picks up process.env.SHELL on Windows, routing spawned commands through Git Bash instead of cmd.exe

scripts/lib/process.mjs (runCommand) and scripts/lib/app-server.mjs (SpawnedCodexAppServerClient.initialize) both do:

shell: process.platform === "win32" ? (process.env.SHELL || true) : false

On a machine where SHELL happens to be set to a POSIX-style shell path (e.g. Git Bash's /bin/bash.exe, as Claude Code's Bash tool sets it), spawnSync/spawn uses that shell instead of the intended cmd.exe. taskkill's own arguments (/PID, /T, /F) then get mangled by MSYS's automatic POSIX-path-to-Windows-path conversion, e.g.:

taskkill /PID 14228 /T /F: exit=1: エラー: 無効な引数またはオプションです - 'C:/Program Files/Git/PID'。

(/PID got rewritten to C:/Program Files/Git/PID.) The same pattern in app-server.mjs causes the codex app-server child (spawned for the shared broker) to sometimes fail to come up cleanly, surfacing as connect ENOENT \\.\pipe\cxc-...-codex-app-server from codex-companion.mjs setup --json.

SHELL is a POSIX convention with no meaning on native Windows process creation; it shouldn't be consulted at all when process.platform === "win32".

Suggested fix
- shell: options.shell ?? (process.platform === "win32" ? (process.env.SHELL || true) : false),
+ shell: options.shell ?? (process.platform === "win32" ? true : false),

(lib/process.mjs, runCommand)

- shell: process.platform === "win32" ? (process.env.SHELL || true) : false,
+ shell: process.platform === "win32" ? true : false,

(lib/app-server.mjs, SpawnedCodexAppServerClient.initialize)

Bug 2 — handleCancel doesn't guard terminateProcessTree, so a non-fatal taskkill failure aborts the whole cancel before job state is ever updated

In codex-companion.mjs, handleCancel:

terminateProcessTree(job.pid ?? Number.NaN);
appendLogLine(job.logFile, "Cancelled by user.");
...
writeJobFile(workspaceRoot, job.id, { ...existing, ...nextJob, cancelledAt: completedAt });
upsertJob(workspaceRoot, { id: job.id, status: "cancelled", ... });

terminateProcessTree (in lib/process.mjs) throws whenever taskkill exits non-zero for a reason other than "process not found" (matched via a narrow regex: /not found|no running instance|cannot find|does not exist|no such process/i). On Windows, taskkill /T frequently fails to kill a subset of grandchild processes with messages that don't match that regex — in our case:

エラー: PID 25692 のプロセス (PID 27196 の子プロセス) を終了できませんでした。
理由: 実行しようとした操作はサポートされていません。

(exit code 128, not a "process not found" message). This throws out of terminateProcessTree, which is called unguarded from handleCancel — so the function aborts before reaching writeJobFile/upsertJob. The job's on-disk status is never flipped to cancelled. From then on, status --all --json shows the job forever "status": "running", and once its last log line happens to be "Turn completed" the derived phase heuristic (inferLegacyJobPhase in lib/job-control.mjs) reports it as stuck in "finalizing" — even though the turn was actually interrupted successfully (visible in the log as Requested Codex turn interrupt for ... / Turn interrupted.).

cancel --json itself also exits 1 in this case (uncaught throw propagates to main().catch()), so from the caller's perspective cancel just "failed," even though the interrupt request that matters (the JSON-RPC turn/interrupt call) had already succeeded.

Suggested fix
- terminateProcessTree(job.pid ?? Number.NaN);
- appendLogLine(job.logFile, "Cancelled by user.");
+ try {
+   terminateProcessTree(job.pid ?? Number.NaN);
+ } catch (error) {
+   const detail = error instanceof Error ? error.message : String(error);
+   appendLogLine(job.logFile, `Process tree termination failed (continuing cancel): ${detail}`);
+ }
+ appendLogLine(job.logFile, "Cancelled by user.");

terminateProcessTree's best-effort semantics for the "process already gone" case already acknowledge that a Windows process kill can fail in ways that shouldn't be fatal to the caller; a partial /T tree-kill failure should get the same treatment rather than aborting job bookkeeping entirely.

Repro steps

  1. On Windows, with a shell environment where SHELL is set to a POSIX shell path (e.g. any session where Claude Code's Bash tool has run, since it's Git Bash-backed).
  2. Start a rescue/task job that takes long enough to still be running a few seconds later (codex-companion.mjs task ... or via the Claude Code plugin's rescue subagent).
  3. node codex-companion.mjs status --all --json --cwd <workspaceRoot> to grab the running job's id.
  4. node codex-companion.mjs cancel <job-id> --cwd <workspaceRoot> --json.
  5. Observe either garbled taskkill output referencing a mangled /PID-as-path, and/or a non-zero exit with the job still "status": "running" on a subsequent status --all --json call.

Local workaround

We patched both spots in our local plugin cache (%USERPROFILE%\.claude\plugins\cache\openai-codex\codex\1.0.6\scripts\...) exactly as shown above and confirmed: (a) taskkill invocations no longer show mangled paths, (b) cancel on a job that hits the Windows partial-tree-kill error now still exits 0 and the job correctly reaches "status": "cancelled" / "phase": "cancelled" in status --all --json. This is obviously wiped on the next plugin update, hence this report.

Related issues

  • #639 describes the same visible symptom (job stuck at running forever after being killed) but locates the cause in lib/tracked-jobs.mjs's lack of a worker-liveness check — a different, likely also-valid bug in a different code path from the ones described here.
  • #634 (Bash 120s auto-background timeout reaping mid-turn) is another distinct cause of the same "job wedged" family of symptoms.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read scripts/lib/process.mjs, especially runCommand and terminateProcessTree, then inspect SpawnedCodexAppServerClient.initialize in scripts/lib/app-server.mjs and handleCancel in codex-companion.mjs. Reproduce with the Windows commands in the issue; done means native taskkill arguments remain intact and cancel still records a cancelled job when process-tree termination fails.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
cli, devtools, operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.