Fix subprocess encoding crash on non-UTF-8 locales (e.g. Windows cp932) - #11
Open
glayht96-ctrl wants to merge 3 commits into
Open
glayht96-ctrl wants to merge 3 commits into
glayht96-ctrl wants to merge 3 commits into
Conversation
Author
|
Pushed an additional commit (f9d7b4e) to this branch. The original audit for this fix searched only for f9d7b4e adds Re-verified after this commit:
|
gqrshy
added a commit
to gqrshy/Codex-Orchestration
that referenced
this pull request
Jul 22, 2026
Strip inherited endpoint and default-model overrides before Claude Fable calls. Force UTF-8 on MCP stdio so Windows locale settings cannot corrupt JSON-RPC payloads. Subprocess encoding remains covered by Cjbuilds#11.
All subprocess.run(..., text=True, ...) calls that read/write Codex or
Claude Code CLI output relied on Python's locale-preferred encoding
(locale.getpreferredencoding()) instead of a fixed one. On Windows with
a Japanese system locale (cp932), any non-cp932 character in the child
process's stdout/stderr (e.g. an em dash in a Claude Fable 5 response)
raises an uncaught UnicodeDecodeError inside CPython's internal
reader/writer thread for the pipe. That exception is not visible to the
surrounding try/except in this codebase (it fires in a background
thread, not the caller), so the MCP server process crashes and the
stdio transport closes without a JSON-RPC response. From the Codex CLI
side this is observed as a bare "Transport closed" error with no
further detail. When the corrupt byte happens to fall in a CP932
double-byte lead/trail range instead of an invalid one, no exception is
raised at all and the text is silently mis-decoded (mojibake) instead.
Reproduced on Windows 11 (ja-JP, cp932) by calling
fable_advisor_mcp.py's review_plan directly over stdio: 2 of 4 natural
(non-adversarial) Claude Fable 5 responses containing an em dash
crashed the server with the UnicodeDecodeError described above; a
third case corrupted the em dash into unrelated CJK characters without
raising anything. Setting PYTHONUTF8=1 before the same 4 calls dropped
the crash rate to 0/4, confirming the encoding is the root cause -
but Codex does not set that variable when it spawns this plugin's MCP
server, so the workaround does not apply in normal use.
Fix: pass encoding="utf-8", errors="replace" explicitly everywhere
text=True is used for a subprocess whose output can contain arbitrary
model-generated or CLI-generated text:
- fable_advisor_mcp.py: _run_json (claude auth status) and
_invoke_fable (claude -p ... --output-format json), the two calls
that read Claude Fable 5's own output and previously crashed.
- inspect_models.py: inspect_version and load_catalog. The existing
`except UnicodeDecodeError` around these calls does not actually
catch this failure mode, since the decode happens in a background
thread; the except clause is left in place as harmless dead code.
- configure_native_routing.py: binary_version, supports_native_policy,
select_fable_server, verify_fable_prerequisites.
- configure_orchestration.py: catalog_source, load_catalog.
Not changed: the macOS-only `cp -a` metadata clone in
configure_orchestration.py's stage_existing_file (guarded by
`os.name == "posix"`), since it never runs on Windows and macOS's
default locale is UTF-8; left out to keep this change minimal.
Testing:
- tests/test_fable_advisor_mcp.py and tests/test_inspect_models.py:
23/23 passed (PYTHONUTF8=1 python -m pytest ...).
- tests/test_native_routing.py and tests/test_configure_orchestration.py:
65 failures both before and after this change (identical set),
all pre-existing POSIX-only assumptions (hardlinks, xattr, fchmod)
unrelated to this fix; no new failures introduced.
- ruff check: passes on all four modified files.
- Manual repro: 4x natural review_plan call without PYTHONUTF8 (2
crashes) vs. 4x with this patch applied, still without PYTHONUTF8
(0 crashes, including responses containing an em dash).
The prior commit (8d0a02d) fixed every subprocess.run(..., text=True, ...) call but only searched for subprocess.run, so it missed the one subprocess.Popen call: AppServer's launch of `codex app-server --stdio` in configure_native_routing.py. That process's stdout pipe is read over the process's full lifetime, so it is exposed to the same cp932 UnicodeDecodeError described in 8d0a02d, on a much longer-lived and more central subprocess than the ones already patched. Fix: add errors="replace" alongside the encoding="utf-8" already present on that call, matching the rest of the codebase.
0.8.0 added external model integration (PR Cjbuilds#14/Cjbuilds#15/Cjbuilds#16/Cjbuilds#23), which introduced 7 more subprocess.run call sites with the same text=True + stdout/stderr=PIPE shape as the ones already patched in 64da77f/262a86c, and therefore the same cp932 UnicodeDecodeError risk on non-UTF-8 locales (e.g. Windows). This adds encoding="utf-8", errors="replace" to all 7: - external_auth_helper.py: _run_capture (1) - external_cli_trust.py: version (1) - external_configurator.py: _user_helper_ready, the two Gate 0 CLI contract checks ("exec --help" / "features list"), and the invoke-CLI-contract help check (4) - external_credentials.py: credential_ready (1) Left untouched, with reasons: - external_auth_helper.py _run_interactive: text=False, stdout/stderr not captured (stdout=None, stderr=None) — nothing to decode. - external_configurator.py _run_invoke_process (subprocess.Popen): stdout=DEVNULL, stderr=DEVNULL — nothing captured, nothing to decode. - external_configurator.py the Gate 0 config-write call: text=False AND stdout/stderr=DEVNULL — doubly excluded. - macOS-only code paths (os.name == "posix" branches) were not touched anywhere in this change, consistent with 64da77f, since macOS's default locale is UTF-8 and this class of crash is Windows/cp932-specific.
glayht96-ctrl
force-pushed
the
fix/cp932-subprocess-encoding
branch
from
July 23, 2026 13:05
f9d7b4e to
cbcd3e7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix subprocess encoding crash on non-UTF-8 locales (e.g. Windows cp932)
Summary
subprocess.run(..., text=True, ...)calls that read/write Codex CLI orClaude Code CLI output rely on Python's locale-preferred encoding instead
of a fixed one. On Windows with a Japanese system locale (
cp932), anycharacter outside
cp932in the child process's stdout/stderr (e.g. an emdash in a Claude Fable 5 response) raises an uncaught
UnicodeDecodeErrorinside CPython's internal reader/writer thread for the pipe.
not caught by this codebase's existing
try/exceptblocks (includingthe
except UnicodeDecodeErroralready present around two of these callsin
inspect_models.py/configure_orchestration.py— that clause is deadcode for this failure mode). The MCP server process crashes and the stdio
transport closes with no JSON-RPC response.
"Transport closed"error with no further detail — it looks like a Codex-side MCP transport
problem, but it is not; the child process itself crashed.
cp932double-byte range instead of an invalid one, no exception is raised at
all, and the text is silently mis-decoded (mojibake) instead of
crashing — a worse failure mode than a visible error.
Repro
Calling
fable_advisor_mcp.py'sreview_plandirectly over stdio onWindows 11 (
ja-JP,cp932), with natural (non-adversarial) prompts toClaude Fable 5:
UnicodeDecodeError: 'cp932' codec can't decode byte 0x94 ...insidesubprocess.py's_readerthread,because the review text happened to contain an em dash.
unrelated CJK characters in the returned JSON.
PYTHONUTF8=1before the same calls dropped the crash rate to0/4, confirming encoding as the root cause. Codex does not set that
variable when it spawns this plugin's MCP server, so the workaround does
not apply during normal use — hence this fix.
Fix
Pass
encoding="utf-8", errors="replace"explicitly on every subprocesscall whose output can contain arbitrary model- or CLI-generated text.
This now covers 18 call sites across 8 files (up from the original 11
across 4 files):
Original 11 (unchanged from before rebase):
fable_advisor_mcp.py:_run_json,_invoke_fableinspect_models.py:inspect_version,load_catalogconfigure_native_routing.py:binary_version,supports_native_policy,select_fable_server,verify_fable_prerequisites, and theAppServersubprocess.Popenlaunch ofcodex app-server --stdio(added in a follow-up commit after the same class of report surfaced
on this long-lived, central subprocess)
configure_orchestration.py:catalog_source,load_catalogNew in this update — 7 more, rebased onto upstream/main (
521f3de):0.8.0 added external model integration (PR #14/#15/#16/#23), which
introduced 7 more
subprocess.runcall sites with the identicaltext=True+stdout/stderr=PIPEshape as the ones above, andtherefore the same cp932 crash risk:
external_auth_helper.py:_run_capture(1)external_cli_trust.py:version(1)external_configurator.py:_user_helper_ready, the two Gate 0 CLIcontract checks (
exec --help/features list), and theinvoke-CLI-contract help check (4)
external_credentials.py:credential_ready(1)Left untouched, with reasons (same principle as the original PR —
only touch sites where output is actually captured and decoded as text):
external_auth_helper.py_run_interactive:text=False,stdout=None/stderr=None— nothing is captured, so nothing isdecoded.
external_configurator.py_run_invoke_process(
subprocess.Popen):stdout=DEVNULL,stderr=DEVNULL— samereason.
external_configurator.py's Gate 0 config-write call:text=Falseand
stdout/stderr=DEVNULL— doubly excluded.os.name == "posix"branches) are still nottouched anywhere in this PR, for the same reason as the original:
they never run on Windows, and macOS's default locale is UTF-8.
This branch has been rebased onto
upstream/mainat521f3de(
Harden Kimi external model invocation (#23)) to pick up the 0.8.0changes before extending the fix to them.
Testing
The manual/real-environment verification below applies to the
original 11 call sites only (the four files listed under "Original
11" above), from before this PR was extended to cover the new external
model integration files:
tests/test_fable_advisor_mcp.py+tests/test_inspect_models.py:23/23 passed (
PYTHONUTF8=1 python -m pytest ...).tests/test_native_routing.py+tests/test_configure_orchestration.py:65 failures both before and after this change, identical set — all
pre-existing POSIX-only assumptions (hardlinks, xattr, fchmod) that don't
hold on Windows, unrelated to this fix. No new failures introduced.
ruff checkpasses on all four modified files.PYTHONUTF8: 4/4 naturalreview_plancalls succeeded, including one response containing an emdash that decoded correctly (previously this exact scenario crashed 2/4
times on the unpatched code).
The 7 new call sites (external model integration files) have not been
verified the same way. They were identified by inspecting every
subprocess.runcall in the four new files and patched only where theshape is structurally identical to an already-verified call
(
text=Truewithstdout=subprocess.PIPEandstderr=subprocess.PIPE,so its output is actually decoded as text). This is a mechanical
application of the same, already-proven fix pattern to structurally
identical call sites — not an independent real-environment reproduction
of a crash on those 7 specific call sites. Verification so far for the
new sites is limited to
python -m py_compile(no syntax errors) onall four touched files; no crash was reproduced or re-tested against
those 7 sites specifically on a cp932 locale.
Environment
identically), system locale
ja-JP(cp932).(UTF-8 is typically already the default locale encoding on those
platforms, and
errors="replace"only changes behavior for bytes thatwere already undecodable).