You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Windows: taskkill /PID is mangled by MSYS path conversion when SHELL is set (Git Bash under Claude Code) — cancel and SessionEnd cleanup never kill the process tree #525
On Windows, terminateProcessTree() invokes taskkill /PID <pid> /T /F through a shell because runCommand defaults to shell: process.env.SHELL || true on win32 (plugins/codex/scripts/lib/process.mjs). When SHELL points to Git Bash — which is exactly the environment Claude Code provides on Windows (SHELL=/bin/bash.exe) — MSYS argument conversion rewrites the /PID switch into a filesystem path, so taskkill fails and the process tree is never killed.
Reproduction
Windows 11, plugin 1.0.6, Node 24.16.0, SHELL=/bin/bash.exe (default under Claude Code's bash tool):
Output (status 1, cp866 mojibake from the Russian locale):
Ошибка: неправильный параметр или аргумент - 'C:/Program Files/Git/PID'.
MSYS has translated /PID → C:/Program Files/Git/PID. With shell: false the same call works correctly.
Impact
/codex:cancel marks a background job cancelled, but the detached task-worker tree survives (the soft turn/interrupt path still works when a turnId is already known, but the hard-kill never lands).
The SessionEnd hook's job cleanup silently fails — cleanupSessionJobs swallows the error, leaking worker processes on every session end.
SpawnedCodexAppServerClient.close() on Windows relies on the same terminateProcessTree, so the shell-wrapped codex app-server grandchild can be leaked too.
Independently of MSYS: looksLikeMissingProcessMessage() matches English-only taskkill output (not found|no running instance|...). On non-English Windows locales taskkill prints localized (OEM-codepage) text, so the "process already gone" detection never matches and dead-process cases surface as thrown errors.
This also shows up when running the repo's own test suite on Windows: the cancel/session-end/status tests fail with the mangled-/PID assertion above (CI currently runs ubuntu-latest only, so it goes unnoticed).
Suggested fix
Run taskkill with shell: false — its arguments are fixed strings, so the shell adds nothing. This mirrors what Remove shell expansion for git commands #447 already did for git ("repository-derived arguments must never pass through a shell"); the same reasoning applies to the win32 default shell: process.env.SHELL || true, which makes behavior depend on whatever SHELL happens to be.
Prefer exit-code-based detection over locale-dependent stderr parsing: taskkill exits with code 128 when the process does not exist.
Summary
On Windows,
terminateProcessTree()invokestaskkill /PID <pid> /T /Fthrough a shell becauserunCommanddefaults toshell: process.env.SHELL || trueon win32 (plugins/codex/scripts/lib/process.mjs). WhenSHELLpoints to Git Bash — which is exactly the environment Claude Code provides on Windows (SHELL=/bin/bash.exe) — MSYS argument conversion rewrites the/PIDswitch into a filesystem path, sotaskkillfails and the process tree is never killed.Reproduction
Windows 11, plugin 1.0.6, Node 24.16.0,
SHELL=/bin/bash.exe(default under Claude Code's bash tool):Output (status 1, cp866 mojibake from the Russian locale):
MSYS has translated
/PID→C:/Program Files/Git/PID. Withshell: falsethe same call works correctly.Impact
/codex:cancelmarks a background jobcancelled, but the detached task-worker tree survives (the softturn/interruptpath still works when aturnIdis already known, but the hard-kill never lands).SessionEndhook's job cleanup silently fails —cleanupSessionJobsswallows the error, leaking worker processes on every session end.SpawnedCodexAppServerClient.close()on Windows relies on the sameterminateProcessTree, so the shell-wrappedcodex app-servergrandchild can be leaked too.looksLikeMissingProcessMessage()matches English-onlytaskkilloutput (not found|no running instance|...). On non-English Windows localestaskkillprints localized (OEM-codepage) text, so the "process already gone" detection never matches and dead-process cases surface as thrown errors.This also shows up when running the repo's own test suite on Windows: the cancel/session-end/status tests fail with the mangled-
/PIDassertion above (CI currently runsubuntu-latestonly, so it goes unnoticed).Suggested fix
taskkillwithshell: false— its arguments are fixed strings, so the shell adds nothing. This mirrors what Remove shell expansion for git commands #447 already did for git ("repository-derived arguments must never pass through a shell"); the same reasoning applies to the win32 defaultshell: process.env.SHELL || true, which makes behavior depend on whateverSHELLhappens to be.taskkillexits with code 128 when the process does not exist.