Summary
/codex:cancel fails to terminate background jobs on Windows when Claude
Code runs under Git Bash (MSYS2), especially with a non-English system locale
(e.g. Korean CP949). Two independent bugs in scripts/lib/process.mjs
terminateProcessTree() combine to leave jobs stuck in the running state
forever.
Environment
- OS: Windows 11
- Shell: Git Bash (MSYS2), invoked via
spawnSync(..., { shell: process.env.SHELL })
- System locale: ko-KR (CP949)
- Plugin:
openai-codex (codex 1.0.3) inside Claude Code
Repro
/codex:rescue --background ... to spawn a background task
/codex:cancel <job-id>
Observed error:
taskkill /PID 19712 /T /F: exit=1:
오류: 잘못된 인수/옵션 - 'C:/Program Files/Git/PID'.
The job's JSON on disk stays "status": "running" forever because cancel
throws before updating the registry.
Root cause
Bug 1 — MSYS argument path conversion
runCommand() passes shell: process.env.SHELL on win32. When the shell
is Git Bash's sh.exe, MSYS2 rewrites any argument that looks like a POSIX
path (/PID, /T, /F) into a Windows path prefixed with the MSYS root, e.g.
C:/Program Files/Git/PID. taskkill then rejects the mangled switch.
Bug 2 — English-only "missing process" detection
Even when taskkill succeeds in reporting that the PID is already gone, on
non-English Windows it prints the message in the system code page (CP949
for Korean) with an exit code of 128. looksLikeMissingProcessMessage() only
matches English phrases, so the "already dead" case is misclassified as a
hard failure, terminateProcessTree throws, and the cancel command never
updates the job status file.
Fix
--- a/scripts/lib/process.mjs
+++ b/scripts/lib/process.mjs
@@ -64,13 +64,18 @@ export function terminateProcessTree(pid, options = {})
{
if (platform === "win32") {
const result = runCommandImpl("taskkill", ["/PID", String(pid), "/T",
"/F"], {
cwd: options.cwd,
- env: options.env
+ env: {
+ ...(options.env ?? process.env),
+ MSYS_NO_PATHCONV: "1",
+ MSYS2_ARG_CONV_EXCL: "*"
+ }
});
if (!result.error && result.status === 0) {
return { attempted: true, delivered: true, method: "taskkill",
result };
}
const combinedOutput = `${result.stderr}\n${result.stdout}`.trim();
- if (!result.error && looksLikeMissingProcessMessage(combinedOutput)) {
+ if (!result.error && (result.status === 128 ||
looksLikeMissingProcessMessage(combinedOutput))) {
return { attempted: true, delivered: false, method: "taskkill",
result };
}
- MSYS_NO_PATHCONV=1 / MSYS2_ARG_CONV_EXCL='*' disable MSYS's argument
path rewriting just for this one child process. Any MSYS-based shell
(Git Bash, MSYS2, Cygwin-derived) honors at least one of these.
- result.status === 128 is taskkill's locale-independent signal for
"process not found", which is exactly the state we want to treat as a
successful no-op during cancel.
Verification
Reproduced on Windows 11 / ko-KR / Git Bash. Before the patch: cancel
throws and the job JSON stays "status":"running". After the patch: cancel returns
Cancelled and the JSON transitions to "status":"cancelled" with
phase:"cancelled" and a cancelledAt timestamp, even when the underlying
PID was already gone.
Summary
/codex:cancelfails to terminate background jobs on Windows when ClaudeCode runs under Git Bash (MSYS2), especially with a non-English system locale
(e.g. Korean CP949). Two independent bugs in
scripts/lib/process.mjsterminateProcessTree()combine to leave jobs stuck in therunningstateforever.
Environment
spawnSync(..., { shell: process.env.SHELL })openai-codex(codex 1.0.3) inside Claude CodeRepro
/codex:rescue --background ...to spawn a background task/codex:cancel <job-id>Observed error:
taskkill /PID 19712 /T /F: exit=1:
오류: 잘못된 인수/옵션 - 'C:/Program Files/Git/PID'.
The job's JSON on disk stays
"status": "running"forever because cancelthrows before updating the registry.
Root cause
Bug 1 — MSYS argument path conversion
runCommand()passesshell: process.env.SHELLon win32. When the shellis Git Bash's
sh.exe, MSYS2 rewrites any argument that looks like a POSIXpath (
/PID,/T,/F) into a Windows path prefixed with the MSYS root, e.g.C:/Program Files/Git/PID.taskkillthen rejects the mangled switch.Bug 2 — English-only "missing process" detection
Even when
taskkillsucceeds in reporting that the PID is already gone, onnon-English Windows it prints the message in the system code page (CP949
for Korean) with an exit code of
128.looksLikeMissingProcessMessage()onlymatches English phrases, so the "already dead" case is misclassified as a
hard failure,
terminateProcessTreethrows, and the cancel command neverupdates the job status file.
Fix
path rewriting just for this one child process. Any MSYS-based shell
(Git Bash, MSYS2, Cygwin-derived) honors at least one of these.
"process not found", which is exactly the state we want to treat as a
successful no-op during cancel.
Verification
Reproduced on Windows 11 / ko-KR / Git Bash. Before the patch: cancel
throws and the job JSON stays "status":"running". After the patch: cancel returns
Cancelled and the JSON transitions to "status":"cancelled" with
phase:"cancelled" and a cancelledAt timestamp, even when the underlying
PID was already gone.