[BUG] Image paste without text creates empty text block; API rejects every subsequent request with 400
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Summary
When a user pastes an image into the CLI with no accompanying text, Claude Code persists the user message as [{type: "image", ...}, {type: "text", text: ""}]. On a later turn, the harness adds a cache_control marker to one of those empty text blocks. The Anthropic API rejects the request with HTTP 400:
messages.<N>.content.<i>.text: cache_control cannot be set for empty text blocks
Because the offending message stays in conversation history, every subsequent request 400s with the same error. The session becomes permanently stuck — /compact fails (its outgoing request includes the bad message), and there is no in-CLI way to recover. The only fix today is killing the CLI and hand-editing the session JSONL.
Environment
- macOS (Darwin 25.4.0)
- Claude.app desktop 1.6608.2 (the CLI was invoked from a terminal subprocess of the app's bundled shell)
claude --version: (please fill in)- Session size at time of failure: ~4.3 MB JSONL, ~507K cached input tokens, ~1,500 events, model
claude-opus-4-7
Severity / impact
High in practice. Once it triggers, the session is unrecoverable from within the CLI — no slash command works, /compact makes it worse, and the only path forward without losing the session is process-kill + manual JSONL editing. Users who don't know how to do that will simply lose their session context.
It is also likely to be hitting other users silently — anyone who pastes a screenshot with no accompanying text is a candidate. Worth checking the issue tracker for related reports filed under generic "API errors" labels.
What Should Happen?
Images should send to the remote session or error out in a non-critical way that blocks the user from continuing the session.
Error Messages/Logs
Steps to Reproduce
Reproduction
- Start a
claudesession in a project. - Take a screenshot. Paste it into the prompt without typing any text. Press Enter.
- Continue the conversation for a few turns until cache breakpoints shift onto the empty text companion block (in my session this took ~2 minutes of normal use after the paste).
- Every assistant turn from that point on returns the synthetic error:
````
API Error: 400 messages.<N>.content.<i>.text: cache_control cannot be set for empty text blocks
In my session the failure cascade started 47 seconds after the paste and produced 12 consecutive synthetic-failure turns before I intervened externally.
What the offending event looks like in the JSONL
{
"uuid": "df37d37a-c629-490e-b0f5-cb648996e3e0",
"type": "user",
"message": {
"role": "user",
"content": [
{ "type": "image", "source": { "type": "base64", "media_type": "image/png", "data": "..." } },
{ "type": "text", "text": "" }
]
}
}
The empty text: "" block is the bug seed. The harness then adds cache_control on top of it when building a later request.
Expected behavior
One of the following:
- Don't emit an empty companion text block. When the user pastes an image with no text, persist just
[{type: "image", ...}]. Image-only user messages are valid in the API. - Don't apply
cache_controlto empty text blocks. When the harness selects cache breakpoints, skip any block wheretext === ""(or where text is missing/null). - At minimum, self-heal on 400. Detect the
cache_control cannot be set for empty text blocksresponse, prune the offender, and retry once — instead of looping forever with no user-facing recovery path.
(1) is the cleanest fix; (2) is a defense-in-depth net; (3) limits blast radius if either of the first two regresses.
Actual behavior
- Empty text block is persisted.
cache_controlends up on it.- API returns 400.
- Every retry includes the same bad message, so every retry 400s.
/compact400s for the same reason (its own request reads the full history).- Synthetic
<synthetic>assistant messages withstop_reason: "stop_sequence"accumulate in the transcript. Thestop_reasonis misleading here — nothing actually triggered a stop sequence; it is the fallback the CLI emits when no real model response was obtained.
Workaround
Externally kill the CLI process, then patch the session JSONL to replace "text": "" with "text": " " (single space) on every offending user event. After resuming, the session works normally.
Sketch:
SESSION=~/.claude/projects/<project-slug>/<session-uuid>.jsonl
cp -p "$SESSION" "$SESSION.bak"
jq -c '
if (.type == "user" or .type == "assistant") and ((.message.content // []) | type == "array")
then .message.content |= map(
if .type == "text" and ((.text // "") == "")
then .text = " " else . end
)
else . end
' "$SESSION" > "$SESSION.tmp" && mv "$SESSION.tmp" "$SESSION" && chmod 600 "$SESSION"
Claude Model
Opus
Is this a regression?
Yes, this worked in a previous version
Last Working Version
_No response_
Claude Code Version
1.6608.2
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
_No response_
This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗