claude --resume fails with 400 'no low surrogate in string' when session JSONL contains truncated emoji
Summary
claude --resume fails with a 400 from the API when the session JSONL contains an orphan UTF-16 high surrogate. The corruption appears to be introduced when a string (in my case a <tool_use_error> payload) is truncated for length and the truncation cuts through the middle of a surrogate pair, leaving a lone \ud83d with no following low surrogate.
Once written to the session file, the session is permanently un-resumable until the file is hand-edited.
Error
API Error: 400 {\"type\":\"error\",\"error\":{\"type\":\"invalid_request_error\",\"message\":\"The request body is not valid JSON: no low surrogate in string: line 1 column 347010 (char 347009)\"},\"request_id\":\"req_011CahqzdVKLuc7m72gTAz6x\"}
Root cause (confirmed)
In the offending session file, a single line contained:
\\\\*[🌤💼🏆📉🎯🚀🔥\ud83d…) errored</tool_use_error>
The \ud83d is the high half of a surrogate pair (the leading code unit for an emoji like 🟢/🔥/etc.) with no following \uDCxx-\uDFxx low surrogate. The … ellipsis right after it strongly suggests the original string was truncated mid-pair by length, not by codepoint.
When the CLI later sends that line back to the API as part of --resume, the JSON deserializer rejects the orphan surrogate.
Repro hint
Trigger any tool call whose error payload contains emoji and is long enough to hit whatever truncation cap is applied to tool_use_error strings. If the cap lands inside a 4-byte UTF-8 character (i.e. inside a surrogate pair when re-encoded as \uXXXX), the session file becomes unresumable.
Suggested fix
Truncate on codepoint / grapheme boundaries rather than raw UTF-16 code units, or sanitize lone surrogates before writing the JSONL line. Either prevents the corruption at the write side. A defensive sanitize at read/resume time would also let already-corrupted sessions recover.
Workaround
Strip lone high surrogates from the offending line in ~/.claude/projects/<dir>/<session>.jsonl:
import re
pat = re.compile(r'\\\\u[dD][89aAbB][0-9a-fA-F]{2}(?!\\\\u[dD][cCdDeEfF][0-9a-fA-F]{2})')
# apply pat.sub('', line) to the bad line, rewrite file
After this the session resumes normally.
Environment
- Claude Code CLI, Opus 4.7
- macOS (Darwin 24.6.0)
- Session file: ~17 MB JSONL, 6131 lines, single bad line at 6101
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗