Bug
On Windows systems with a non-English (specifically zh-TW / CP-950 / Big5) system locale, /codex:review, /codex:task, and the stop-time review gate fail with:
Failed to parse codex app-server JSONL: Unexpected token '�', "���\: PID "... is not valid JSON
The companion process exits with status 1 and the connection is torn down. Subsequent retries are racy: they sometimes succeed (when an existing broker happens to absorb the request), but a fresh broker spawn reliably reproduces the failure.
Reproduction
- Windows + zh-TW (Traditional Chinese / Taiwan) system locale (
chcp returns 950).
- Have any project that starts the codex CLI with at least one MCP server that fails to start (e.g. an offline server, a misconfigured one, or one with handshake errors). On my machine
unityMCP, cheatengine, and ida-pro-mcp all fail; any one of them is enough.
- Tear down any running broker (
%LOCALAPPDATA%\Temp\cxc-*\broker.pid) so the next companion invocation has to spawn a fresh one.
- From the project directory:
node "<plugin-cache>/scripts/codex-companion.mjs" task --json "anything"
The companion fails immediately with the parse error above. Re-running the same command after a successful broker is up succeeds — broker mode does not exercise the leaky path.
Root cause
The JSONL parser in plugins/codex/scripts/lib/app-server.mjs handleLine() calls JSON.parse() on every line received from codex.exe's stdout pipe and, on failure, calls handleExit() which rejects every in-flight request and tears down the connection.
Capturing raw bytes from inside app-server-broker.mjs's internal SpawnedCodexAppServerClient (full hex dump below), the offending line is not corrupted JSON — it is a complete localized Windows OS message that has nothing to do with the protocol:
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 the success message printed by taskkill /T /F /PID xxxx — Windows' built-in process-tree killer — under the system locale (CP-950 / Big5). The companion process itself does not call taskkill on this code path, so the bytes have to be coming from inside codex.exe itself.
Walking the app-server-broker.mjs log on the failing run confirms it (timestamps abridged):
SPAWN proc.pid=77412 transport=direct
STDOUT len=209 "{\"id\":1,\"result\":{\"userAgent\":\"...\",\"codexHome\":...}}" ← initialize ok
STDOUT len=94 "{\"method\":\"remoteControl/status/changed\",...}" ← ok
STDOUT len=652 "{\"method\":\"mcpServer/startupStatus/updated\",...starting...}" ← ok
STDERR len=53 "���~: ��..." // CP-950 "錯誤: 找不到..." (taskkill ENOENT path)
STDOUT len=246 "{\"method\":\"mcpServer/startupStatus/updated\",...failed...}" ← ok
...
STDOUT len=111 "���\: PID �� 70260 (PID �� 1396 ��...) ..." ← LEAK
BAD LINE transport=direct ← parser tears the connection down
The leak appears immediately after a mcpServer/startupStatus/updated-failed notification. The most plausible explanation is that the upstream codex CLI invokes taskkill to clean up an MCP child whose handshake failed, and incorrectly leaves taskkill's stdout connected to its own stdout (instead of capturing it or redirecting to NUL). On en-US Windows the leaked line happens to be ASCII (SUCCESS: ...) but still not JSON, so issue #23-style failures (different first byte) are the same bug class.
This is therefore primarily an upstream codex-CLI bug. I'll file it at openai/codex separately, link from here.
Why this is distinct from #23 / #24 / #97 / #171
#23 reports the same outer symptom (Failed to parse codex app-server JSONL) but the leak source there is shell-init noise (\x1b[?2004h, bracketed paste mode). The proposed fixes (#24, #97, #171) all add stripAnsi() and re-parse. That regex would not match the bytes above — they are not ANSI escapes, they are localized OS text — so JSONL parsing still fails after stripping.
Both bug classes share the same correct shape of fix at the plugin layer, though: lines that cannot possibly be JSONL records should be dropped, not treated as protocol violations that tear the connection down. A JSONL record always begins with { or [. Any line whose first non-whitespace, post-stripAnsi character is something else is definitively garbage from outside the protocol.
A PR that implements that one-line guard (and subsumes #23) is incoming.
Why it's racy
Once a broker is alive, all subsequent connections from the companion go over the named pipe and never spawn a direct codex.exe in the parent process — so the leaky stdout never flows through the JSONL parser of the parent. The leak is still happening inside the broker process, but is parsed there and fails the same way; the broker just dies more quietly.
This also explains why running the same command twice in a row often shows different outcomes (first call: broker bootstrap fails on the leak; second call: existing broker handles it).
Environment
|
|
| OS |
Windows 11 Pro 26200, zh-TW system locale (CP-950) |
| Shell |
PowerShell 7 + Git Bash (both reproduce) |
| Node |
v22.16.0 |
| codex CLI |
0.130.0 |
| codex-plugin-cc |
1.0.4 |
| Reproducer |
runs taskkill ⇒ Big5 success line bytes a6 a8 a5 5c 3a 20 50 49 44 ... |
Workaround
Disable the stop-time review gate (/codex:setup --disable-review-gate) until a fix lands, OR keep a long-lived broker alive (do not let %LOCALAPPDATA%\Temp\cxc-* go stale).
Bug
On Windows systems with a non-English (specifically zh-TW / CP-950 / Big5) system locale,
/codex:review,/codex:task, and the stop-time review gate fail with:The companion process exits with status 1 and the connection is torn down. Subsequent retries are racy: they sometimes succeed (when an existing broker happens to absorb the request), but a fresh broker spawn reliably reproduces the failure.
Reproduction
chcpreturns950).unityMCP,cheatengine, andida-pro-mcpall fail; any one of them is enough.%LOCALAPPDATA%\Temp\cxc-*\broker.pid) so the next companion invocation has to spawn a fresh one.The companion fails immediately with the parse error above. Re-running the same command after a successful broker is up succeeds — broker mode does not exercise the leaky path.
Root cause
The JSONL parser in
plugins/codex/scripts/lib/app-server.mjshandleLine()callsJSON.parse()on every line received fromcodex.exe's stdout pipe and, on failure, callshandleExit()which rejects every in-flight request and tears down the connection.Capturing raw bytes from inside
app-server-broker.mjs's internalSpawnedCodexAppServerClient(full hex dump below), the offending line is not corrupted JSON — it is a complete localized Windows OS message that has nothing to do with the protocol:That is the success message printed by
taskkill /T /F /PID xxxx— Windows' built-in process-tree killer — under the system locale (CP-950 / Big5). The companion process itself does not calltaskkillon this code path, so the bytes have to be coming from insidecodex.exeitself.Walking the
app-server-broker.mjslog on the failing run confirms it (timestamps abridged):The leak appears immediately after a
mcpServer/startupStatus/updated-failednotification. The most plausible explanation is that the upstream codex CLI invokestaskkillto clean up an MCP child whose handshake failed, and incorrectly leaves taskkill's stdout connected to its own stdout (instead of capturing it or redirecting toNUL). On en-US Windows the leaked line happens to be ASCII (SUCCESS: ...) but still not JSON, so issue #23-style failures (different first byte) are the same bug class.This is therefore primarily an upstream codex-CLI bug. I'll file it at
openai/codexseparately, link from here.Why this is distinct from #23 / #24 / #97 / #171
#23 reports the same outer symptom (
Failed to parse codex app-server JSONL) but the leak source there is shell-init noise (\x1b[?2004h, bracketed paste mode). The proposed fixes (#24, #97, #171) all addstripAnsi()and re-parse. That regex would not match the bytes above — they are not ANSI escapes, they are localized OS text — so JSONL parsing still fails after stripping.Both bug classes share the same correct shape of fix at the plugin layer, though: lines that cannot possibly be JSONL records should be dropped, not treated as protocol violations that tear the connection down. A JSONL record always begins with
{or[. Any line whose first non-whitespace, post-stripAnsicharacter is something else is definitively garbage from outside the protocol.A PR that implements that one-line guard (and subsumes #23) is incoming.
Why it's racy
Once a broker is alive, all subsequent connections from the companion go over the named pipe and never spawn a direct codex.exe in the parent process — so the leaky stdout never flows through the JSONL parser of the parent. The leak is still happening inside the broker process, but is parsed there and fails the same way; the broker just dies more quietly.
This also explains why running the same command twice in a row often shows different outcomes (first call: broker bootstrap fails on the leak; second call: existing broker handles it).
Environment
taskkill⇒ Big5 success line bytesa6 a8 a5 5c 3a 20 50 49 44 ...Workaround
Disable the stop-time review gate (
/codex:setup --disable-review-gate) until a fix lands, OR keep a long-lived broker alive (do not let%LOCALAPPDATA%\Temp\cxc-*go stale).