Edit: after an escaped-form match, every non-ASCII character in new_string is written as a \uXXXX escape

Status Open
Reported on v2.1.286
Maintainer reply None cached
Activity 0 comments · opened Oct 4, 2026

What's wrong

When Edit finds old_string only in escaped form (the fallback behind the "Edit also tried swapping \uXXXX escapes and their characters" note), it writes every non-ASCII character in new_string as a \uXXXX escape, not only the ones it had to swap in old_string. Text that is new in new_string, such as Hangul, emoji (as two surrogate escapes) or accented letters, lands escaped, and the tool reports success.

One escaped character in old_string is enough, even when the file uses literal non-ASCII text elsewhere. A typical trigger: a test file holds \u200b as text (escaped because it is invisible), the model copies that line into old_string, the input repair pass from #72957 turns the escape into U+200B so only the escaped form matches, and the line added next to it is escaped too.

Steps to reproduce

  1. A file with literal Hangul on one line and an escape on the next:

``sh
printf 'const a = "한글";\nconst z = "\\u200b";\n' > repro.js
``

  1. Read it, then Edit with
  • old_string: const z = "\u200b";
  • new_string: that line, a newline, then const k = "한글 🏝️ é";
  1. Result:

``
$ cat repro.js
const a = "한글";
const z = "\u200b";
const k = "\ud55c\uae00 \ud83c\udfdd\ufe0f \u00e9";
``

Control: the same line added after an ASCII-only old_string is written as typed.

What should happen

const k = "한글 🏝️ é";, as typed. Keeping the escaped form for the code points that were escaped in the matched text is right; characters that only appear in new_string have no form in the file to keep. In 2.1.286 they get a fresh escape in the file's hex case, which looks meant to follow a file that escapes all non-ASCII, but nothing checks that the file does: this one has literal Hangul on line 1.

Impact

Silent, and harmful outside JavaScript. In JS, Java and JSON the escapes compare equal at run time, so tests still pass and the change shows only in review. In Python, "\ud83c\udfdd" is two lone surrogates, not 🏝, and raises UnicodeEncodeError when encoded as UTF-8. Go rejects the surrogate-half escapes and Rust the unbraced form at compile time, and in Markdown, comments or plain YAML the reader gets \ud55c\uae00 instead of the text. I hit this in a real project, where a new test's Korean strings were all rewritten.

Workaround

Anchor the edit on text without the escape, such as the next line, so the exact match succeeds and the fallback never runs.

Environment

  • Claude Code 2.1.286, desktop app (Code tab), macOS
  • Model: Opus 5.5

Related: #64479 (the same fallback, all-or-nothing on old_string), #72957 (the repair pass that makes escaped-form matches common), #52813.

View original on GitHub ↗