openai / openai/codex-plugin-cc
Windows: SHELL env var (Git Bash) breaks taskkill; handleCancel swallows terminateProcessTree exceptions, leaving jobs stuck in running/finalizing
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.SHELLis set to/bin/bash.exefor 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
- On Windows, with a shell environment where
SHELLis set to a POSIX shell path (e.g. any session where Claude Code's Bash tool has run, since it's Git Bash-backed). - 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). node codex-companion.mjs status --all --json --cwd <workspaceRoot>to grab the running job's id.node codex-companion.mjs cancel <job-id> --cwd <workspaceRoot> --json.- Observe either garbled
taskkilloutput referencing a mangled/PID-as-path, and/or a non-zero exit with the job still"status": "running"on a subsequentstatus --all --jsoncall.
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
runningforever after being killed) but locates the cause inlib/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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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