[BUG] Image paste without text creates empty text block; API rejects every subsequent request with 400

Resolved 💬 5 comments Opened May 13, 2026 by ngmeyer Closed Jun 21, 2026

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

  1. Start a claude session in a project.
  2. Take a screenshot. Paste it into the prompt without typing any text. Press Enter.
  3. 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).
  4. 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:

  1. 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.
  2. Don't apply cache_control to empty text blocks. When the harness selects cache breakpoints, skip any block where text === "" (or where text is missing/null).
  3. At minimum, self-heal on 400. Detect the cache_control cannot be set for empty text blocks response, 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_control ends up on it.
  • API returns 400.
  • Every retry includes the same bad message, so every retry 400s.
  • /compact 400s for the same reason (its own request reads the full history).
  • Synthetic <synthetic> assistant messages with stop_reason: "stop_sequence" accumulate in the transcript. The stop_reason is 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_

View original on GitHub ↗

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