Skip to content

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

Open

Description

@lask3802

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.

Activity

  1. added
    bugSomething isn't working
    CLIIssues related to the Codex CLI
    windows-osIssues related to Codex on Windows systems
    app-serverIssues involving app server protocol or interfaces
    mcpIssues related to the use of model context protocol (MCP) servers
    on May 9, 2026
  2. removed
    CLIIssues related to the Codex CLI
    app-serverIssues involving app server protocol or interfaces
    on May 9, 2026
  3. mitre88 commented on May 9, 2026

    @mitre88

    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: 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.

  4. lask3802 commented on May 9, 2026

    @lask3802
    Author

    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 Windows Codex Desktop: taskkill stdout is parsed as AppServerConnection JSON-RPC/MCP message #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.

  5. lask3802 commented on May 10, 2026

    @lask3802
    Author

    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 fix(tui): suppress taskkill output for MCP teardown on Windows #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 Add better logging to rejection error #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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingmcpIssues related to the use of model context protocol (MCP) serverswindows-osIssues related to Codex on Windows systems

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions