Windows: codex app-server leaks taskkill stdout into its own stdout (corrupts JSONL on non-English locales)

Open 💬 4 comments Opened May 9, 2026 by lask3802
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

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: taskkill still 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:

  1. Capture stdout / stderr explicitly. Set Stdio::null() for stdout when spawning taskkill — the cleanup code only needs the exit status, not the success text.
  2. 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.
  3. Use the Win32 API directly. OpenProcess + TerminateProcess for the target PID, and CreateToolhelp32Snapshot to 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-cc 1.0.4 and via a standalone cargo-built consumer that just spawns codex app-server and reads stdout.

View original on GitHub ↗

4 Comments

github-actions[bot] contributor · 3 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #21658
  • #20869
  • #20845

Powered by Codex Action

mitre88 · 3 months ago

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.rs still invokes taskkill /PID <pid> /T /F without redirecting stdio.
  • codex-rs/exec-server/src/connection.rs has a second Windows taskkill call in kill_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-client callsite but was closed unmerged. This branch includes that same direction and also covers the exec-server callsite, which seems relevant to the app-server/JSONL corruption described here.

Validation run locally:

  • cargo fmt --package codex-exec-server --package codex-rmcp-client --check
  • cargo check -p codex-exec-server -p codex-rmcp-client
  • cargo test -p codex-rmcp-client process_group -- --nocapture → 1 passed
  • cargo test -p codex-exec-server connection -- --nocapture → completed successfully; no tests matched that filter
  • git diff --check

I can open this as the invited PR if this is the route maintainers want.

lask3802 · 3 months ago

Cross-references after a closer search of existing tickets:

  • This issue is substantively a duplicate of #21658, which is more comprehensive: en-US SUCCESS: T... parse failure, GBK / Simplified Chinese mojibake, attached log excerpt, and a deterministic Windows minimal reproducer (probe-host-cleanup.js).
  • #20845 is the same root cause surfacing in /mcp TUI output. #20869 was the earlier closed report.
  • #21690 (closed unmerged) was a partial fix limited to the rmcp-client callsite.

What this issue contributes that the others do not:

  • A CP-950 / Big5 (zh-TW) byte-level reproduction (a6 a8 a5 5c 3a 20 50 49 44 ...) and the resulting UTF-8 mojibake ���\: PID ... — distinct from the GBK example in #21658.
  • The downstream fallout in 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 of codex app-server against this and adjacent terminal-noise leaks.
  • Confirmation that two Windows taskkill callsites are involved — codex-rs/rmcp-client/src/stdio_server_launcher.rs and codex-rs/exec-server/src/connection.rs — both currently spawn without redirected stdio. (Thanks @mitre88 for verifying this on main and 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.

lask3802 · 3 months ago

Upstream status update:

  • ✅ #21759 merged at e5d0222 (2026-05-10) — applies Stdio::null() to the LocalProcessTerminator taskkill in codex-rs/rmcp-client/src/stdio_server_launcher.rs. This is exactly the callsite hit by the failure I reported (the MCP cleanup path triggered by unityMCP / cheatengine / ida-pro-mcp failing to start). Once a Codex CLI release includes that commit, the CP-950 / Big5 leak stops at the source.
  • ⏳ Adjacent callsite still inherits stdio: 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-line Stdio::null() pattern as #21759.
  • The plugin-side guard at openai/codex-plugin-cc#311 remains useful as defense-in-depth: it drops any non-JSONL line on the codex app-server stdout, so future leaks from any source (a third taskkill callsite, terminal noise like #23, etc.) won't tear the connection down. Independent of the upstream stdio-isolation fixes.

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.