[BUG] Sonnet 5 writes Korean tool-call parameters as \uXXXX escapes and mis-spells the hex, corrupting Hangul (100% corruption when escape-writing engages; likely root cause of #80009 / #78996)

Status Open
Reported on v2.1.220
Maintainer reply ✓ Yes — bcherny
Activity 6 comments · opened Aug 1, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Summary

We root-caused the frequently-reported Korean (Hangul) corruption in tool-call parameters (AskUserQuestion, TodoWrite — see #80009, #78996): Claude Sonnet 5 sometimes writes Korean tool-parameter strings as \uXXXX unicode escape sequences instead of literal UTF-8, and mis-spells the hex codepoints while doing so. Each mis-spelled escape renders as a valid-but-wrong Hangul syllable — exactly the "syllable substitution producing non-words" symptom in the linked issues.

In a controlled A/B experiment, every single run in which the model engaged escape-writing produced corrupted Hangul (45/45, 100%), at a density of ~3–5% of syllables, while literal-writing runs were clean of this class. A simple output-format instruction fully suppresses it (workaround below).

Root cause evidence

Controlled A/B experiment (2026-07-31)

  • Model: claude-sonnet-5 (via Amazon Bedrock global.anthropic.claude-sonnet-5, direct ConverseStream API — no CLI in the loop, raw input_json_delta captured pre-decode; the spontaneous behavior was originally observed in interactive Claude Code sessions on both 1P and Bedrock backends).
  • Scenario: forced Korean tool call (AskUserQuestion-style schema: question ≥6 sentences, 3 options with Korean descriptions).
  • Arm E: system prompt requires \uXXXX escapes for Korean in tool JSON. Arm L: system prompt forbids escapes (literal UTF-8). n=30/arm, interleaved. Replicated on a second AWS account to rule out account/pool effects.
  • Detection: KS X 1001 rare-syllable heuristic + U+FFFD + escape-validity checks on the decoded text (escapes themselves are not counted as corruption), plus an LLM judge for common-syllable substitutions.

| | Account 1 | Account 2 |
|---|---|---|
| Arm E, runs that actually used escapes | 23/23 corrupted (100%) | 22/22 corrupted (100%), incl. 1 invalid-JSON run |
| Arm L (literal) | 1/30 (a distinct issue, see "Residual" below) | 0/30 |
| two-proportion z (E vs L) | +7.01 | +7.21 |

Runs that partially engaged (as few as 6 Hangul escapes) also corrupted. The deciding variable is not the instruction but whether escapes were actually written.

What the mis-spellings look like (real raw output, Arm E)

Raw tool-input JSON text as emitted by the model (single-level escapes), with decodes:

...SSO 로그인 점깔도 완릷된 상황...
→ "SSO 로그인 점깔도 완릷된 상황"   (intended: "SSO 로그인 점검도 완료된 상황")

...작업을 실제로 코딅하기 전에...
→ "작업을 실제로 코딅하기 전에"     (intended: "...코딩하기 전에")

Mis-spelled codepoints observed: 검 U+AC80 → 깔 U+AE54, 료 U+B8CC → 릷 U+B9B7, 딩 U+B529 → 딅 U+B505, plus many landing on non-KS-X-1001 syllables (릷, 딅, 렜, 뱠, …). Writing each syllable as four hand-spelled hex digits gives the model many opportunities to emit a near-miss codepoint; literal UTF-8 writing does not.

Why session logs can't show the fingerprint (note for #80009)

Session JSONL stores tool_use.input as an already-parsed object, so model-written escapes are decoded away before storage — the transcript shows substituted syllables with no trace of the escapes. To capture the fingerprint, use a raw source: claude -p ... --output-format stream-json --include-partial-messages and inspect the partial_json strings (model-written escapes survive there as \\uXXXX in file-raw), or a logging proxy at the wire. Rule of thumb: count backslash layers at the capture point — double backslash in file-raw = the model wrote the escape.

Workaround (verified)

Adding this instruction (memory / CLAUDE.md / system prompt) fully suppressed the corruption class in the field and in the experiment:

Always write Korean (and other non-ASCII) strings in tool-call parameters as literal UTF-8; never as \uXXXX unicode escapes.

Residual, distinct low-rate issue (not fixed by the workaround)

Separately from the escape class, we verified rare literal token-level syllable substitutions (~0.3–0.4% of runs; e.g. 새벽→새벅, 믿고→믄고, 쪽→쪼르) that arrive as clean literal UTF-8 in the raw stream (zero escapes) — the workaround cannot and does not suppress these. We also observed one U+FFFD emission replacing a syllable inside tool-input JSON on the 1P API path (single replacement character with intact continuation, which excludes client-side chunk-split decoding). Happy to file these separately with full raw delta captures if useful.

Environment

  • Claude Code CLI 2.1.220 (behavior observed on both 1P/Enterprise and Bedrock backends; reproduced via direct Bedrock API without any CLI, so this is model output behavior, not a client bug)
  • Model: claude-sonnet-5
  • Korean-language sessions, tool-heavy workflows (AskUserQuestion / TodoWrite)

Suggested fix

Model/decoding-side: avoid hand-spelling \uXXXX hex for CJK in tool-input JSON (emit literal UTF-8), and/or validate/normalize escape sequences in tool-parameter strings at generation time. Full harness code, raw run logs (JSONL with pre-decode deltas), and adversarial verification records are available on request.

Related: #80009, #78996 (symptom reports consistent with this root cause).

View original on GitHub ↗

6 Comments

portlogics-wonypark · 2 months ago

Field report: same failure mode at far higher density (non-engineer user, AskUserQuestion)

Another instance from our company, this time from an ops colleague's interactive session (Sonnet), not a controlled run. Same surface as the linked reports — an AskUserQuestion prompt — but the corruption density is well past the ~3–5% in the root-cause section above: nearly every content word is a wrong syllable, so the question and its options aren't merely misspelled, they're unreadable. A native Korean speaker cannot reconstruct what is being asked.

<img width="621" height="303" alt="Image" src="https://github.com/user-attachments/assets/fe72366c-e175-4ca7-a693-d445641e9aeb" />

What survives intact is what you'd expect if the model were hand-spelling hex for Hangul only:

  • ASCII and digits are clean: Other, Submit answers, 9시·13시·16시, 9/13/16시
  • Function words and a few common syllables survive — the question line reads 뽁일 기벘이뛌 어느 시점에 확인할까요?, where only the tail 어느 시점에 확인할까요? is well-formed
  • Content nouns are almost entirely substituted, and a large share land on syllables outside KS X 1001 (뛌, 뾯, 뾩, 뽁, 첤 — as read off the screenshot), matching the fingerprint described above

Two things this case adds:

  1. Severity isn't bounded by the 3–5% density. AskUserQuestion is a blocking UI: the turn cannot proceed until the user selects an option, and here there is nothing legible to select. The only recourse is to abandon the turn.
  2. This is the default experience for non-engineer users. No CLAUDE.md workaround was in place. Ops users are the least equipped to recognize "the model mis-spelled a unicode escape" and the most likely to conclude the tool is broken — which, for this turn, it effectively is.

Evidence grade: screenshot only. The session ran on a colleague's machine and the JSONL wasn't retained, so I can't produce raw pre-decode deltas for this run. Reporting it as a severity data point, not as additional root-cause evidence.

changwonjeon · 2 months ago

I also saw the problem frequently.

jaewoosong · 2 months ago

As far as I know, this is a very important issue for East Asian language users.

bcherny collaborator · 2 months ago

Thanks for the unusually thorough writeup — the raw-delta capture and A/B numbers are exactly what's needed here.

What we tried on the released CLI 2.1.233 (macOS): three non-interactive runs (claude -p --model sonnet --output-format stream-json --include-partial-messages) asking Sonnet 5 to build a 6–8 item Korean TodoWrite list, then inspecting the raw partial_json deltas. All three runs came back as literal UTF-8 with zero \uXXXX escapes and no substituted syllables. That's consistent with your finding that the escape-writing is intermittent rather than a CLI-side transform.

On the client side, Claude Code decodes whatever escapes the model emits faithfully, so a mis-spelled hex codepoint necessarily lands as a wrong Hangul syllable — the CLI can't repair that. Your evidence that this reproduces against the Bedrock API with no CLI in the loop points at model output behavior; we've passed this issue (and #80009 / #78996) to the model team, and we're also looking at whether the harness should steer the model toward literal UTF-8 in tool parameters, which you report fully suppresses the class. Until then, the CLAUDE.md instruction in your workaround is the right mitigation.

If anyone hits this again, a --output-format stream-json --include-partial-messages capture showing the escapes in partial_json on the current version would help confirm it's still occurring.

🤖 Generated with Claude Code

Lucid-Engineering · 1 month ago

Additional reproduction data point — also occurs on Opus 5, Claude Code CLI, 2026-08-19.

An AskUserQuestion call whose arguments were emitted as \uXXXX escapes corrupted one syllable:

  • Intended: 멀쩡하게 (… U+BA40 U+C9F5 U+D558 U+AC8C …)
  • Emitted escapes: 멀잩하게 → rendered as 멀잩하게 ('쩡' U+C9F5 → '잩' U+C7A9)

The corruption is present in the serialized tool_use input itself (session-log level), consistent with this issue's mis-spelled-hex root cause — not terminal rendering. Common syllables in the same payload (불러올·밝혔·뿔) survived; the rarer syllable broke.

Separately, a user of ours reported the U+FFFD variant back in July: "회의 후 불러올 사람이 누구인가요?" displayed with '불' (U+BD88) replaced, and the serialized JSON contained a literal � — i.e. the character was lost at argument-generation time. That observation also fits escape-miswriting rather than UTF-8 byte-boundary truncation.

Working mitigation for us: instructing the model (via skill/system prompt) to write Korean tool arguments as raw UTF-8 instead of \uXXXX escapes.

YourBestDeveloper · 1 month ago

Same failure mode on a surface not mentioned above — MCP write tools (Notion notion-update-page,
Jira editJiraIssue). Caught live today:

잡히지  →  잡힐지
  '히' U+D788   choseong 18, jungseong 20, jongseong 0
  '힐' U+D790   choseong 18, jungseong 20, jongseong 8

Only the final consonant flips, so the result is still a well-formed Hangul syllable — the write
succeeds, no error, and it reads as a typo afterwards.

I put together a Claude Code plugin as a stopgap:
hangul-corruption-guard — a
PreToolUse hook that diffs outgoing Korean against a reference file staged beforehand and blocks
the write when they diverge. Since the corrupted output is a valid syllable, having something to
compare against is the only way to see it.

A mitigation, not a fix — posting in case it is useful to anyone else hitting this.