Resume fails with "no low surrogate in string" — cancelled-Bash error formatter truncates command mid-emoji, poisoning the session transcript

Resolved 💬 2 comments Opened Jun 24, 2026 by gavingmiller Closed Jun 28, 2026

Summary

When a Bash tool call whose command contains a non-BMP character (e.g. an emoji) is cancelled, Claude Code's cancellation-error formatter truncates the command string with by character count and can slice through a UTF-16 surrogate pair. The resulting tool_result is written to the session JSONL with a lone high surrogate escape (\uD83D with no \uDCxx partner). Once that line is in the transcript, every subsequent /resume on that session fails with HTTP 400 from the API because the request body is no longer valid JSON (RFC 8259 §7 forbids unpaired surrogates).

The session is effectively bricked — you can't get past the first turn.

Symptom

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 199313 (char 199312)"}}

Same column on every resume regardless of what the user types, because the bad byte is at a fixed offset in the replayed transcript, not the new input.

Root cause

A line in the session JSONL contains:

{"type":"tool_result",
 "content":"<tool_use_error>Cancelled: parallel tool call Bash(ls -la /Users/.../Omnibus/\ud83d…) errored</tool_use_error>",
 "is_error":true,
 "tool_use_id":"toolu_..."}

📥 is U+1F4E5 → UTF-16 surrogate pair 📥. The formatter cut the string between the two halves and appended , leaving \uD83D orphaned. The same value also appears in the sibling toolUseResult field on that line.

Repro

  1. Run a Bash tool call whose command contains a non-BMP character near the truncation length used by the cancellation formatter — e.g. ls -la ~/some/path/📥-folder/with-enough-extra-characters-to-hit-the-truncation-boundary.
  2. Cancel it (ESC) while it's queued/running.
  3. End the session.
  4. /resume it. The next user turn returns HTTP 400 with the surrogate error.

Fix suggestion

Two layers:

  1. Formatter — when truncating a tool-call argument with , slice by code point and back off one position if the slice lands inside a surrogate pair (or inside a grapheme cluster more generally). In JS: don't slice raw UTF-16 code units; iterate [...str] or use Intl.Segmenter.
  2. Defensive serializer — before POSTing the transcript, scan for unpaired surrogates in all string fields and either drop them or replace with U+FFFD. This prevents any upstream bug from bricking a session.

A user-facing recovery path would also help: today the only fix is to hand-edit the JSONL.

Workaround

Edit ~/.claude/projects/<project>/<session>.jsonl, find the line(s) containing \ud83d… (or any \uD8xx… / \uD9xx… / \uDAxx… / \uDBxx… followed by something other than \uDCxx\uDFxx), and remove the orphaned high surrogate.

Environment

  • Claude Code CLI on macOS (darwin 23.6.0)
  • Model: claude-opus-4-7 (1M context)
  • Session file format: JSONL under ~/.claude/projects/<project>/

View original on GitHub ↗

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