Write/Edit tools silently decode \uXXXX in file content, corrupting escape-sequence text (Linux)

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

Summary

The Write and Edit tools silently decode \uXXXX sequences in file content as JSON Unicode escapes before writing to disk. As a result, it is impossible to store a literal \uXXXX escape sequence in a file via these tools:

  • Authoring the six-character text \uE010 writes the actual U+E010 character.
  • Authoring \\uE010 to compensate writes a doubled backslash (\\uE010).

Neither produces the intended single-backslash literal \uE010 on disk.

This is the content-corruption analog of several closed Windows-path reports (#58358, #54583, #51815). Those were framed around home-directory paths on Windows and closed as stale/duplicate. This report is about arbitrary file content on Linux, is trivially reproducible, and still occurs, so I am filing fresh per the maintainer note on #54583 ("Please open a new issue if this is still relevant").

Why it matters

Any workflow that legitimately needs literal \uXXXX text in a file is silently corrupted:

  • Source code / test fixtures that assert on escape-sequence strings.
  • Config or data files that store Unicode escapes as text (e.g. a YAML field holding \uE010).
  • Documentation about escape sequences (this very issue body hit the bug while being drafted).

The corruption is invisible: the written characters often render as nothing or as unrelated glyphs, Read shows an apparently empty "", and grep for the intended text cannot find the line. The agent cannot tell it went wrong without dropping to od -c.

Reproducer (Linux)

Call the Write tool so that the file content is the line tengwar: " followed by the six literal characters \uE010, then \uE046, then ". Inspect the raw bytes:

$ od -c file.txt
0000000   t   e   n   g   w   a   r   :       " 356 200 220 356 201 206
0000020   "  \n

356 200 220 = UTF-8 for U+E010, 356 201 206 = U+E046. The escape text was decoded into the actual characters before the file was written.

Attempted workaround — author the content with doubled backslashes (\\uE010\\uE046):

$ od -c file2.txt
0000000   t   e   n   g   w   a   r   :       "   \   \   u   E   0   1
0000020   0   \   \   u   E   0   4   6   "  \n

That yields a doubled backslash, not the intended single-backslash escape. So there is no way to write a literal single-backslash \uXXXX through Write/Edit.

Note this is not clean JSON round-tripping: standard JSON would decode a doubled backslash to a single one, but here the doubled backslash is preserved literally while a bare \uXXXX is decoded. The two behaviors are inconsistent.

Working alternative (for reference)

Writing the same value via Bash with sed/printf produces the correct single-backslash escape on disk (the shell tool's own escape handling collapses the doubled backslash). So the corruption is specific to how \uXXXX in the Write/Edit content argument is decoded at the tool boundary, not an inherent filesystem limitation.

Expected

The content (and Edit new_string) should be written to disk byte-for-byte as authored. A literal single-backslash escape in the intended file content should appear on disk as the six ASCII characters backslash, u, E, 0, 1, 0, not be decoded into U+E010.

Environment

  • Platform: Linux
  • Tools affected: Write, Edit (content / new_string). The related Edit old_string matching case is #52813.
  • Appears to be a transport/argument-decoding issue (JSON Unicode-escape decoding of tool arguments), so likely platform-independent.

Related (but closed)

  • #58358 — Write/Edit/Bash decode escapes in paths/heredoc content (closed as duplicate of #54583)
  • #54583 — escape in Windows path decoded (closed, not planned, stale; invites a new issue)
  • #52813 — Edit silently normalizes escapes in old_string, breaking exact matches

Related (still open)

  • #51815 — Read/Edit fail on Windows username with an escape sequence
  • #64479 - Edit tool fails on mixed literal/escape Unicode in multi-line old_string
  • #73483 - Write emits raw U+0000 where the model intended the six-character NUL escape sequence
  • #66058 - Edit tool corrupts literal escape sequences

🤖 Filed with Claude Code

View original on GitHub ↗

3 Comments

bcherny collaborator · 2 months ago

Thanks for the detailed repro. Ran it on 2.1.233 (Linux) and reproduced the symptom exactly: asking for the literal six characters \uE010 via Write produced the 3-byte character ee 80 90 on disk, and escaping as \\uE010 produced a doubled backslash. Same with Edit.

However, the raw stream-json output shows the Write/Edit tools are not doing the decoding: the string in the tool call had already become the single U+E010 character before the tool ran, and when the tool did receive a two-backslash string it wrote two backslashes to disk byte-for-byte. Tool arguments travel as JSON, and the model is emitting the JSON unicode-escape form ("\uE010" — which is the character) when the intent is literal text, so the six characters never reach the tool.

So: confirmed and reproduced on 2.1.233, but the root cause is in how the model encodes its tool arguments, not in the Write/Edit tools themselves — we're routing it accordingly. In the meantime, the reliable workaround is to have Claude write such content via a shell command (e.g. printf/heredoc), which is what it eventually did in our runs.

🤖 Generated with Claude Code

moshekar · 1 month ago

Same problem on MCP tool arguments, on Windows.

Sending a tool argument intended to contain the literal characters \ u 2 0 1 C:
\u201C, \\u201C and \\\\u201C all arrive at the server as “. I found no
escaping form that gets the literal through — doubling does not help at this surface.

Btw, \\\\u201C does get through in a Bash command, but still decoded to “ in MCP tool arguments.

Attrexx · 26 days ago

Still reproducible, and wider than Write/Edit on Linux.

Scope. On Windows 11 Pro 10.0.26220 (Git Bash, pwsh 7) the \uXXXX decode happens in the
Bash tool, the PowerShell tool and the Write tool alike, so it is not a Write/Edit bug. It sits
upstream of tool dispatch.

Evidence that it is upstream. A PreToolUse hook that inspects tool_input never sees the
escape: a quoted heredoc body containing \u03b1 reaches the hook with the escape already
decoded, and the file lands with the character. By contrast the Windows backslash collapse of
#88561 is visible to such a hook. The two rewrites are therefore at different layers, and this
one cannot be guarded or fixed at the tool level.

Decoder behaviour, measured through the Bash tool with od -c:

| input | result |
| --- | --- |
| \u0041 | A |
| \u00e9 | U+00E9 |
| \u4e2d | U+4E2D |
| \ud83d\ude00 | U+1F600, surrogate pair correctly combined |
| \uZZZZ | unchanged |
| \n \t \" \b \r \0 \x41 | all unchanged |

Correct surrogate-pair combining points at a real JSON string unescaper rather than a regex, applied
to a string that has already been JSON-decoded once. Only \u is handled, which is why it stays
unnoticed: every escape people check first looks fine.

A workaround exists on Windows, which I think answers the open question in the issue body
("neither produces the intended single-backslash literal"). On Linux with Write/Edit, no. But the
Bash tool on Windows can write a literal \uXXXX, by doubling the backslash. This tool call:

cat > probe.json <<'JEOF'
{"greek": "\\u03b1", "path": "C:\\\\Users"}
JEOF

produces on disk, verified with od -c:

{"greek": "\u03b1", "path": "C:\\Users"}

which is correct JSON for both fields. The doubling survives because the Bash-specific collapse of
#88561 also applies on this path; I am reporting that as an empirical recipe rather than a claim
about the relative order of the two passes, since the PowerShell tool preserves doubled backslashes
while still decoding \u, and I cannot see the internals.

So on Windows the two bugs partly cancel, and the Bash tool is the only one of the three that can
emit a literal \uXXXX at all.

Showing cached comments. Read the full discussion on GitHub ↗