[BUG] claude-opus-5 substitutes wrong Hangul syllables in generated text (other models: 0 occurrences in 31,542 messages)
Summary
When using claude-opus-5, Korean text in model output intermittently contains wrong but individually valid Hangul syllables. This is not mojibake, not U+FFFD replacement, and not a terminal rendering issue — the characters are well-formed Korean that simply isn't the word the model meant to write.
I have ~37k assistant messages across 131 session transcripts on one machine, using four models over the same period with the same environment. The corruption appears only with claude-opus-5.
Evidence: model comparison on identical environment
Counted from ~/.claude/projects/**/*.jsonl (type: assistant, grouped by message.model),
using the script below:
| Model | assistant messages | messages containing corruption | rate | first seen |
|---|---:|---:|---:|---|
| claude-opus-4-8 | 15,491 | 0 | 0.00% | — |
| claude-sonnet-5 | 13,924 | 0 | 0.00% | — |
| claude-fable-5 | 2,127 | 0 | 0.00% | — |
| claude-opus-5 | 3,088 | 38 | 1.23% | 2026-07-28 |
Same user, same machine, same terminal, same prompts/rules, overlapping days. Only the model differs.
Two notes on how this was counted, both in the conservative direction:
- Cut off at 2026-07-29. Once I started documenting this bug I quoted the corrupted strings
in files and commit messages, which would inflate the opus-5 count. Including those days
gives 87/4,963 (1.75%); the table uses the clean window instead.
- Narrow patterns only. Counting a substituted syllable requires knowing it in advance, so
1.23% is a floor, not the true rate — substitutions I never noticed are not counted.
Symptom examples
Each pair is (what appeared) → (what was meant). Both sides are valid Hangul.
| Appeared | Intended | Note |
|---|---|---|
| 하시게습니까 | 하시겠습니까 | "would you like to…" — by far the most frequent |
| 하십시까 | 하십니까 | |
| 로여 | 로컬 | "local" |
| 자객증명 | 자격증명 | "credentials" |
| 턴밀널 | 터미널 | "terminal" |
| 원경 병합 | 원격 병합 | "remote merge" |
| 엄겪하지만 | 엄격하지만 | "strict, but…" |
| 커미 / 켤및 | 커밋 | "commit" |
| 버진 | 빠진 | "missing" |
| 재생생 | 재생성 | "regenerated" |
Real examples from transcripts (before I was even aware of the problem, so these are not hand-written samples):
"question": "wired 메모리 11GB를 어떻게 처리하시겊습니까?"
"추적을 로여에 남기고 지금은 올리지 않습니다"
"정합성·자객증명 검사도 통과해 fast-forward"
"세션·턴밀널 직접 커미함"
"원경 병합은 이미 마쳤고(rebase)"
There is no discernible rule to the substitution — not codepoint adjacency, not jamo similarity, not keyboard proximity. This makes client-side blocklists ineffective.
What this is NOT
I checked existing issues; this is a different failure mode from all of them:
| Existing issue | Its symptom | Why mine differs |
|---|---|---|
| #46863 | Hangul → U+FFFD replacement char | Mine produces valid Hangul, never U+FFFD |
| #40396, #41358 | VS Code integrated terminal rendering | Mine is in the stored transcript, not just on screen |
| #42538, #65806, #42899 | Windows UTF-8 → CP1252 mojibake | macOS; no byte-level mojibake |
| #11695 | /export encoding | Occurs in normal output and tool arguments |
Ruled out locally:
- Terminal / font / renderer — the corrupted characters are already corrupted in the on-disk transcript and in hook logs, before anything is displayed.
- Terminal multiplexer (I use cmux + tmux, which colleagues do not) — installed 2026-03-20, corruption started 2026-07-28. Timing does not match.
- My own writing habit /
\uXXXXescaping in tool arguments — corruption also appears in plain assistant text, where no JSON escaping is involved. - A specific tool — see below.
- A client update — the CLI binary did not change across the boundary. Native installs keep
old versions on disk (~/.local/share/claude/versions/), so the update history is recoverable
from file timestamps:
````
2.1.218 2026-07-23 06:31
2.1.219 2026-07-25 02:22
2.1.220 2026-07-25 10:37 <- still current
So 2026-07-25 through 07-27 ran 2.1.220 with zero corruption, and 07-28 onward ran
the same 2.1.220 with corruption. The only thing that changed on 07-28 was the model.
Where it occurs
Not limited to one tool. On 2026-07-28 alone, corruption appeared in:
AskUserQuestion arguments 8
Bash arguments 8
Edit arguments 4
Write arguments 1
plain assistant text 3
It is most visible in AskUserQuestion because the text is rendered as UI the user must read and act on — a corrupted option label is confusing in a way that a corrupted sentence in prose is not.
Timeline
AskUserQuestion calls per day with corruption count, all sessions:
2026-06-25 .. 2026-07-27 700+ calls 0 corrupted (sonnet-5 primary)
2026-07-28 32 calls 8 corrupted (opus-5 becomes primary)
2026-07-29 71 calls 3 corrupted
2026-07-30 65 calls 7 corrupted
claude-opus-5 first appears in my transcripts on 2026-07-27 (59 messages, 0 corrupted), then 738 messages on 07-28, 2,291 on 07-29, 1,819 on 07-30.
The CLI binary was 2.1.220 for this entire window (installed 07-25), so the 07-27/07-28
boundary isolates the model as the only changed variable.
How to check your own transcripts
This counts, per model, how many assistant messages contain any of a list of known-bad strings. Replace the pattern with substitutions you have actually observed in your own language.
Keep the patterns narrow — a substituted syllable can coincidentally appear inside a legitimate
word. (로여 alone matches the perfectly valid 새 경로여야, which produced one false positive
for me until I anchored it to the following particle.)
import json, os, re, glob
from collections import defaultdict
# Replace with substitutions you have observed. These are Korean examples,
# anchored to following characters to avoid matching legitimate words.
PAT = re.compile(r'하시겊|하시갰|로여(에|만|로)|자객증명|턴밀널|원경 병합|엄겪|게습니|십시까')
tot, bad = defaultdict(int), defaultdict(int)
for f in glob.glob(os.path.expanduser("~/.claude/projects/*/*.jsonl")):
for ln in open(f, encoding="utf-8", errors="replace"):
try:
e = json.loads(ln)
except Exception:
continue
if e.get("type") != "assistant":
continue
msg = e.get("message") or {}
m = msg.get("model")
if not m:
continue
tot[m] += 1
if PAT.search(json.dumps(msg.get("content"), ensure_ascii=False)):
bad[m] += 1
for m in sorted(tot, key=lambda k: -tot[k]):
print(f"{m:<28} {tot[m]:>7} msgs {bad[m]:>4} corrupted {100*bad[m]/tot[m]:.2f}%")
Environment
- Claude Code 2.1.220 — installed 2026-07-25, i.e. 3 days before the corruption began
and unchanged since (see the version-history note above)
- macOS 26.5.2 (arm64)
- Terminal: cmux + tmux (ruled out above; corruption is in the transcript, not the display)
- Language: all prompts, rules and output in Korean (a global instruction file enforces Korean responses)
Impact
- User-facing UI text is wrong.
AskUserQuestionoption labels and questions are what the user reads to make a decision; a corrupted label is worse than a corrupted sentence. - It propagates into files and commits. Corruption in
Edit/Writearguments lands in source files, documentation, and commit messages. I found it in committed docs written earlier this week. - Client-side mitigation does not converge. With ~11,172 possible Hangul syllables and no rule to the substitution, a blocklist only catches repeats. I built one and it was bypassed by new substitutions three times in a single session.
- Workaround that does work: switch models (
/model).sonnet-5andopus-4-8show 0 occurrences over 29,415 messages combined.
Why this may be underreported
claude-opus-5 is recent, and this only becomes visible if you generate large volumes of CJK text with it. My colleagues on the same machines and same workflows do not see it because they are not on opus-5. At ~1.2% per message, someone writing a few Korean sentences a day would dismiss it as a typo rather than file a bug.
I am happy to provide additional counts or anonymized transcript excerpts if useful.
3 Comments
Note on labels / template
This issue has no label because I filed it with
gh issue create --title --body-file, whichbypasses the web issue templates that auto-apply labels. Apologies — I only realized afterwards.
For triage: this belongs under
model, notbug. The corruption is in the model's generatedtext, not in the CLI — the same client version (2.1.220, installed 3 days before onset) produced
zero occurrences with
sonnet-5,opus-4-8andfable-5over 31,542 messages.I did not refile through the
🤖 Model Behavior Issuetemplate because its required fields(
Type of Behavior Issue,Permission Mode,Files Affected) are aimed at unwanted fileoperations rather than output-quality defects, and forcing this report into them would make it
harder to read rather than easier. I would rather not create a duplicate. Happy to refile in
whatever shape is most useful if a maintainer prefers that.
I cannot add the label myself (
heestore does not have the correct permissions to execute
AddLabelsToLabelable``), so this is a request rather than an action.Following up: the triage run this comment triggered
(https://github.com/anthropics/claude-code/actions/runs/30546800350) failed
before doing any work — it hit
The action has timed outright after ClaudeCode initialized (5m21s total). Other recent triage runs on this repo
(.github#5, #449, #426, #397) show the same timeout pattern around the same
time, so this looks like an infra issue on the triage workflow rather than
anything specific to this issue.
Re-posting to retrigger — still requesting the
modellabel per the note above.Correction to the claim above ("other models: 0 occurrences in 31,542 messages"): that count
only searched for the specific patterns I'd already observed from
opus-5output, so it wasn'ta real test of "does this happen on other models" — just "does this exact corruption happen".
I found a counterexample. In a session that used
claude-sonnet-5exclusively (noopus-5atall, 281 assistant messages total), a single
AskUserQuestioncall contained three distinctHangul substitutions in one message:
| Appeared | Intended (best guess) |
|---|---|
| 옥길까요? | 옮길까요? ("...move it too?") |
| 뇌백이 쉽습니다 | not a real word — likely "되돌리기" ("to revert") or similar |
| 심볼릭릭크 (appeared twice) | 심볼릭 링크 ("symbolic link") |
None of these match the substitution patterns I'd catalogued from
opus-5(하시게습니까,로여, 자객증명, etc.) — this looks like a separate, sonnet-5-specific set of substitutions,
not the same bug recurring. Re-scanning my full transcript history (~36k messages, all models)
for these new patterns turned up 0 occurrences anywhere except this one sonnet-5 session.
So: the opus-5 frequency finding (1.23% vs 0% for the patterns tracked at the time) still
stands as measured, but "other models: 0 occurrences" was narrower than it read — it meant
zero for the patterns I knew to look for, not zero for the underlying phenomenon. It's possible
this is a general low-frequency Hangul-substitution issue that opus-5 simply triggers far more
often, rather than something exclusive to it. Flagging so this isn't taken as stronger evidence
of model-exclusivity than the data actually supports. Sample size here is 1 (three substitutions
within one call), so I can't estimate a real rate for sonnet-5 yet.