Skip to content

Fix taskkill cancellation on Windows Git Bash (MSYS path mangling + non-English locale) #219

@MemorialStar

Description

@MemorialStar

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

  1. /codex:rescue --background ... to spawn a background task
  2. /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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions