Session JSONL corruption: persisted bash output truncation splits Unicode surrogate pairs, breaks --resume

Resolved 💬 4 comments Opened Apr 10, 2026 by mercurialsolo Closed Apr 13, 2026

Summary

Claude Code's "persisted output" truncation for long bash command output can cut a string in the middle of a UTF-16 surrogate pair, leaving a lone high surrogate (e.g. \ud83c) in the session .jsonl file. When the session is later resumed, the history is replayed to the API and the lone surrogate makes the request body invalid JSON, producing:

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 111140 (char 111139)"}}

The failure is deterministic — once a session file contains the broken character, every --resume of that session 400s until the file is patched or the session is abandoned.

Reproduction

  1. Run a bash command whose output contains an emoji (e.g. 🏷 = U+1F3F7, encoded as surrogate pair \ud83c\udff7) and is long enough to trigger the persisted-output truncator.
  2. End / compact the session.
  3. claude --resume that session.
  4. First request → 400 invalid JSON.

Evidence from affected session files

Three session files under ~/.claude/projects/<project>/ contained the same broken persisted output, all at .message.content[0].content char 2211:

...Make Videos with Your AI Avatar  \ud83c\n...\n</persisted-output>...

The 🏷 emoji's high surrogate \ud83c was kept; the low surrogate \udff7 was dropped; the \n...\n truncation marker was appended in its place. This strongly suggests the truncator is slicing by raw character/byte index without checking whether it's landing inside a surrogate pair (or, more generally, inside a multi-code-unit grapheme).

Expected behavior

Truncation should be Unicode-aware. Either:

  • Slice on grapheme cluster boundaries, or at minimum
  • Refuse to slice between a high surrogate (U+D800U+DBFF) and the following low surrogate (U+DC00U+DFFF); back off one position.

A defense-in-depth fix would also sanitize lone surrogates at the session-write boundary, e.g. in JS:

s.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, '\uFFFD')

Impact

  • Any user whose bash output contains emoji near the truncation boundary can end up with an unresumable session and silent loss of work context.
  • The error message points at the API request body, not at the session file, so the cause is non-obvious — users will assume it's an Anthropic API outage.

Workaround

Strip lone surrogates from the offending session JSONL:

text.encode('utf-16','surrogatepass').decode('utf-16','ignore')

Environment

  • Claude Code, model claude-opus-4-6 (1M context)
  • macOS Darwin 25.3.0
  • Reproduced across three separate sessions on the same project

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗