Windows: codex app-server leaks taskkill stdout into its own stdout (corrupts JSONL on non-English locales)
Summary
codex (Rust binary) on Windows leaks the stdout of an internally-spawned taskkill /T /F /PID <pid> onto its own stdout. On non-English Windows locales (e.g. zh-TW / CP-950 / Big5), the leaked line is OS-localized text that breaks any consumer parsing the codex stdout as JSONL.
The downstream fallout is documented in openai/codex-plugin-cc #310 — codex app-server's JSONL stream is corrupted, and the plugin's parser tears the connection down.
Reproduction
| | |
|--|--|
| OS | Windows 11 Pro 26200, system locale Traditional Chinese / Taiwan (chcp = 950) |
| codex | codex-cli 0.130.0 |
| Trigger | start codex app-server, request a thread that pulls in any MCP server config that fails to start (handshake error / missing binary / dead transport) |
The corrupted line on stdout is reliably:
hex : a6 a8 a5 5c 3a 20 50 49 44 20 ac b0 20 31 32 33 34 ...
cp950 : 成功: PID 為 1234 (PID 為 5678 的子處理程序) 的處理程序已終止。
utf-8 : ���\: PID �� 1234 (PID �� 5678 ���l�B�z�{��) ...
That is exactly what taskkill /T /F /PID xxxx prints to its own stdout on a zh-TW Windows console — and it is appearing on the codex app-server JSONL stdout, not on stderr, not in a side-channel.
Evidence: timing
Captured by attaching a raw-byte logger to the codex stdout pipe inside the embedding process (timestamps abridged):
T+0.0 STDOUT {"id":1,"result":{...}} ← initialize ok
T+0.1 STDOUT {"method":"remoteControl/status/changed",...} ← ok
T+7.6 STDOUT {"method":"mcpServer/startupStatus/updated",...starting} × N
T+7.8 STDERR ��� ~ : ... 找不到 ... ← Big5 "錯誤" path
T+7.8 STDOUT {"method":"mcpServer/startupStatus/updated","...failed..."} ← ok
...
T+9.6 STDERR rmcp::transport::worker quit with fatal: Transport channel closed ...
T+9.7 STDOUT {"method":"mcpServer/startupStatus/updated","name":"unityMCP","status":"failed","error":"... Server returned error: ..."}
...
T+9.9 STDOUT ���\: PID �� 70260 (PID �� 1396 ���l�B�z�{��) ... ← LEAK
The leak fires exactly after the codex CLI gives up on a failed MCP server and has to clean its child process tree. So the path that calls taskkill to terminate a misbehaving / orphaned MCP child is also the path that leaks the killer's stdout.
The leak is not present on:
- en-US Windows:
taskkillstill prints, but the bytes happen to be ASCII (SUCCESS: The process with PID 1234 ...) so JSONL parsers still get a parse error but the failure mode is much harder to attribute. - macOS / Linux: the cleanup path doesn't shell out to a PID killer.
- runs where every MCP server starts cleanly: no cleanup, no leak.
Suggested fix
Audit the Windows-only process-cleanup path that invokes taskkill. Options:
- Capture stdout / stderr explicitly. Set
Stdio::null()for stdout when spawningtaskkill— the cleanup code only needs the exit status, not the success text. - Pipe and discard. If output is needed for diagnostics, capture into a
Vec<u8>and (a) decode as the active OEM codepage rather than UTF-8 before logging, (b) write to the codex log file rather than stdout. - Use the Win32 API directly.
OpenProcess+TerminateProcessfor the target PID, andCreateToolhelp32Snapshotto walk children for the/T-equivalent. No subprocess, no stdio to manage.
(1) is the smallest patch and definitively fixes the symptom.
Why this surfaces in plugins, too
The codex-plugin-cc embedding parses codex app-server stdout as JSONL. When this leak fires, the parser sees a non-JSON line and tears the connection down (Failed to parse codex app-server JSONL: Unexpected token '�', "���\: PID "...). I have a corresponding plugin-side guard incoming at openai/codex-plugin-cc — it drops lines whose first non-whitespace character is not { or [, which protects every embedder from this and from similar terminal-noise leaks (e.g. zsh bracketed-paste, also reported as openai/codex-plugin-cc#23). That guard is, however, defense-in-depth; the right place to fix the corruption is here.
Environment
- Windows 11 Pro 26200 (zh-TW system locale, CP-950 console)
- codex 0.130.0 (npm
@openai/codex) - Reproduces via
codex-plugin-cc1.0.4 and via a standalonecargo-built consumer that just spawnscodex app-serverand reads stdout.
4 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
I validated the current code paths mentioned here and prepared a narrow branch in case maintainers want the small stdio-isolation fix.
Branch: https://github.com/mitre88/codex/tree/fix/silent-windows-taskkill-cleanup
Compare: https://github.com/openai/codex/compare/main...mitre88:codex:fix/silent-windows-taskkill-cleanup
What I found on current
main:codex-rs/rmcp-client/src/stdio_server_launcher.rsstill invokestaskkill /PID <pid> /T /Fwithout redirecting stdio.codex-rs/exec-server/src/connection.rshas a second Windowstaskkillcall inkill_windows_process_tree, also without redirecting stdio.The branch keeps the existing process-tree cleanup behavior but makes both Windows helper invocations silent:
stdin(Stdio::null())stdout(Stdio::null())stderr(Stdio::null())creation_flags(CREATE_NO_WINDOW)I noticed #21690 covered the
rmcp-clientcallsite but was closed unmerged. This branch includes that same direction and also covers theexec-servercallsite, which seems relevant to the app-server/JSONL corruption described here.Validation run locally:
cargo fmt --package codex-exec-server --package codex-rmcp-client --checkcargo check -p codex-exec-server -p codex-rmcp-clientcargo test -p codex-rmcp-client process_group -- --nocapture→ 1 passedcargo test -p codex-exec-server connection -- --nocapture→ completed successfully; no tests matched that filtergit diff --checkI can open this as the invited PR if this is the route maintainers want.
Cross-references after a closer search of existing tickets:
SUCCESS: T...parse failure, GBK / Simplified Chinese mojibake, attached log excerpt, and a deterministic Windows minimal reproducer (probe-host-cleanup.js)./mcpTUI output. #20869 was the earlier closed report.rmcp-clientcallsite.What this issue contributes that the others do not:
a6 a8 a5 5c 3a 20 50 49 44 ...) and the resulting UTF-8 mojibake���\: PID ...— distinct from the GBK example in #21658.codex-plugin-cc(filed as openai/codex-plugin-cc#310) and a defense-in-depth plugin-side fix in openai/codex-plugin-cc#311, which protects every embedder ofcodex app-serveragainst this and adjacent terminal-noise leaks.taskkillcallsites are involved —codex-rs/rmcp-client/src/stdio_server_launcher.rsandcodex-rs/exec-server/src/connection.rs— both currently spawn without redirected stdio. (Thanks @mitre88 for verifying this onmainand preparing a branch.)Happy to close this in favour of #21658 if maintainers prefer to consolidate; the CP-950 evidence and the plugin cross-link can move there as a comment. The two issues describe the same defect.
Upstream status update:
e5d0222(2026-05-10) — appliesStdio::null()to theLocalProcessTerminatortaskkill incodex-rs/rmcp-client/src/stdio_server_launcher.rs. This is exactly the callsite hit by the failure I reported (the MCP cleanup path triggered byunityMCP/cheatengine/ida-pro-mcpfailing to start). Once a Codex CLI release includes that commit, the CP-950 / Big5 leak stops at the source.codex-rs/exec-server/src/connection.rs::kill_windows_process_tree(called when the exec-server tears down its own subprocess tree, distinct from MCP cleanup). @mitre88 has a narrowed branch ready and asked on #21658 for an invited PR. Same three-lineStdio::null()pattern as #21759.Happy to close this in favour of #21658 whenever maintainers prefer. The CP-950 byte evidence above is the unique contribution; everything else is now subsumed by #21658 / #21759.