[BUG] claude-opus-5 substitutes wrong Hangul syllables in generated text (other models: 0 occurrences in 31,542 messages)

Status Open
Reported on v2.1.218
Maintainer reply None cached
Activity 3 comments · opened Jul 30, 2026

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 / \uXXXX escaping 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. AskUserQuestion option 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/Write arguments 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-5 and opus-4-8 show 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.

View original on GitHub ↗

3 Comments

heestore · 1 month ago

Note on labels / template

This issue has no label because I filed it with gh issue create --title --body-file, which
bypasses the web issue templates that auto-apply labels. Apologies — I only realized afterwards.

For triage: this belongs under model, not bug. The corruption is in the model's generated
text, not in the CLI — the same client version (2.1.220, installed 3 days before onset) produced
zero occurrences with sonnet-5, opus-4-8 and fable-5 over 31,542 messages.

I did not refile through the 🤖 Model Behavior Issue template because its required fields
(Type of Behavior Issue, Permission Mode, Files Affected) are aimed at unwanted file
operations 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.

heestore · 29 days ago

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 out right after Claude
Code 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 model label per the note above.

heestore · 28 days ago

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-5 output, so it wasn't
a 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-5 exclusively (no opus-5 at
all, 281 assistant messages total), a single AskUserQuestion call contained three distinct
Hangul 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.