taskkill /PID flag mangled by Git Bash MSYS path translation on Windows
Summary
terminateProcessTree() fails silently on Windows when the Node.js process is launched from Git Bash (e.g. via the Claude Code CLI or any MSYS2-based terminal). The cancel companion command returns exit 1 and running processes are never stopped.
Root cause
In scripts/lib/process.mjs, runCommand() sets:
shell: process.platform === "win32" ? (process.env.SHELL || true) : false,
In a Git Bash session, process.env.SHELL is /usr/bin/bash (or similar). spawnSync then spawns that shell as the interpreter. Git Bash applies MSYS automatic path translation to all arguments that look like Unix paths: /PID → C:/Program Files/Git/PID, /T → C:/Program Files/Git/T, /F → C:/Program Files/Git/F.
So the actual command that reaches taskkill.exe is:
taskkill "C:/Program Files/Git/PID" 65784 "C:/Program Files/Git/T" "C:/Program Files/Git/F"
which exits 1 with ERROR: Invalid argument/option - 'C:/Program Files/Git/PID'.
Reproduction
- Open Git Bash on Windows.
- Launch Claude Code (or any tool that uses
codex-companion.mjs). - Start a long-running codex task and try to cancel it.
cancel <job-id>returns an error; the process keeps running.
Confirmed on: Windows 11 Pro 10.0.26200, Git for Windows 2.x, Node.js 20+.
Error output (observed)
taskkill /PID 65784 /T /F: exit=1: ERROR: Invalid argument/option - 'C:/Program Files/Git/PID'
Workaround
Set MSYS_NO_PATHCONV=1 before invoking the companion:
MSYS_NO_PATHCONV=1 node scripts/codex-companion.mjs cancel <job-id>
Suggested fix
For the taskkill invocation specifically, either:
Option A — force cmd.exe instead of process.env.SHELL for Win32 so MSYS translation never runs:
// In runCommand():
shell: process.platform === "win32" ? "cmd.exe" : false,
Option B — inject MSYS_NO_PATHCONV=1 into the env when calling terminateProcessTree():
const result = runCommandImpl("taskkill", ["/PID", String(pid), "/T", "/F"], {
cwd: options.cwd,
env: { MSYS_NO_PATHCONV: "1", ...(options.env ?? process.env) },
});
Option C — use shell: false for taskkill directly (it is a native Windows executable and does not need a shell wrapper). This would require passing a custom runCommandImpl or adding a noShell option to runCommand.
Option A is the lowest-risk single-line change; cmd.exe is always available on Win32 and does not apply path mangling.
2 Comments
Confirmed on a current Windows setup.
Environment:
C:\Program Files\Git\bin\bash.exeI reproduced this with a temporary
PING.EXEprocess created solely for the test. From Git Bash:So
/PIDis still being rewritten by MSYS path conversion exactly as described in this issue. The user-visible effect is that cancellation fails and the long-running job/process remains alive.I also verified that prefixing the command with
MSYS_NO_PATHCONV=1removes the argument-mangling error. In the ChatGPT Windows sandbox the corrected command then reachestaskkill.exebut returnsAccess denied; directtaskkill.exefrom PowerShell returns the same access-denied result there, so that second result appears to be the sandbox boundary rather than MSYS conversion. The temporary processes were cleaned up with PowerShellStop-Process.This suggests the companion should avoid
process.env.SHELLfor native Windows process termination and invoketaskkill.exewithshell: false(or forcecmd.exe), while the app-side runner should use its native process-tree termination path rather than relying on a sandboxedtaskkill.One more Windows localization edge case should be covered by the upstream fix and its regression tests.
On Spanish Windows, invoking
taskkill.exefor a nonexistent PID produces:The current
looksLikeMissingProcessMessage()matcher in the Claude Code Codex plugin only recognizes English phrases:As a result, once the Git Bash argument-mangling problem is fixed, idempotent cancellation can still throw on localized Windows installations when the process has already exited.
A tempting workaround is to accept every
taskkillexit code 128 as “already stopped”, but that is broader than necessary and can hide a different localized failure. A safer minimal fix is to extend the message matcher, for example:I verified this locally with two regression cases:
ERROR: no se encontró el proceso "1234".returns an already-stopped outcome;ERROR: Acceso denegado.still throws.Please include a localized missing-process test with the upstream
shell: false/ nativetaskkillfix so this behavior survives plugin updates.