[Bug] Read on mislabeled .png causes unrecoverable 'Could not process image' session corruption

Status Fixed / completed
Reported on v2.1.104
Maintainer reply ✓ Yes — claude[bot]
Activity 8 comments · opened Apr 14, 2026 · closed May 19, 2026
💡 Likely answer: A maintainer (claude[bot], contributor) responded on this thread — see the highlighted reply below.

Preflight Checklist

  • [x] I have searched existing issues and found related reports, but this includes a more precise root cause and reproduction shape
  • [x] This is a single bug report
  • [x] I am using the latest Claude Code version involved in the failure (session recorded on 2.1.104)

What's Wrong?

Claude Code can permanently corrupt a session when the Read tool is used on a file with an image extension that is not actually an image file on disk.

In my case, a file named flywheel-diagram.png was created by a local export endpoint, but the file contents were actually JSON text:

{"success":true,"format":"png","data":"<base64 PNG bytes>"}

Claude Code appears to infer from the .png path that the file is an image, then sends it as an image content block. The API rejects it with:

API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"Could not process image"}}

After that, the bad image stays in context and every subsequent turn fails with the same error. The session becomes unusable until I rewind the session history.

Why this is a distinct bug shape

There are existing reports for:

  • valid-but-unprocessable images
  • oversized images
  • image attachments causing unrecoverable loops

This report is narrower and more actionable:

a file with .png extension contained JSON text, and Claude Code still turned it into an image block instead of rejecting it as non-image content.

So there appear to be two failures:

  1. Input validation failureRead should verify that a .png file is actually a PNG before emitting an image block
  2. Recovery failure — once the image is rejected by the API, Claude Code should drop that image from context instead of poisoning the session forever

Steps to Reproduce

  1. Create a file named something like foo.png
  2. Put JSON text into it instead of PNG bytes, for example:
{"success":true,"format":"png","data":"iVBORw0KGgoAAAANSUhEUgAA..."}
  1. Ask Claude Code to Read that file
  2. Claude Code emits an image content block
  3. Anthropic API returns:
API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"Could not process image"}}
  1. Send any follow-up message
  2. The same error repeats on every turn; session is effectively bricked

Expected Behavior

Claude Code should do one or more of:

  1. Inspect file signatures / MIME instead of trusting the filename extension alone
  2. If the file is not a real image, return text/metadata instead of image content
  3. If the API rejects an image block, remove that image from context for subsequent turns
  4. Allow /compact, /resume, or normal follow-up prompts to proceed after surfacing the failure

Actual Behavior

  • Read on the mislabeled .png caused an image block to enter context
  • API returned 400 Could not process image
  • every subsequent turn repeated the same error
  • the only recovery was rewinding the session

Concrete Evidence from Session

Session details:

  • Claude Code version in transcript metadata: 2.1.104
  • Platform: Linux

Observed sequence:

  • export command wrote .../flywheel-diagram.png
  • Claude Code then executed Read on that path
  • the Read tool result contained an image block whose base64 payload decoded to JSON, not raw PNG bytes
  • decoding that outer JSON's data field produced a valid PNG
  • local inspection of the file with file reported: JSON text data
  • Pillow could not open it as an image

In other words, the file on disk was not a PNG even though it had a .png extension.

Representative Error

API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"Could not process image"},"request_id":"req_011Ca3skZB4K7sqrsbuQfnCB"}

After that, later turns in the same session also failed with the same message until rewind.

Environment

  • Claude Code CLI
  • Version: 2.1.104
  • OS: Linux (Ubuntu)
  • Working with a local file path read via the built-in Read tool

Suggested Fix

For Read on image-like paths:

  1. Validate magic bytes / MIME before constructing an image content block
  2. If file contents are text/JSON, treat them as text even if the extension is .png
  3. If image processing fails upstream, quarantine/drop the offending image block from future requests so the session can recover

Related Issues

This seems closely related to other unrecoverable image-context failures, but the mislabeled-file-path case may make the root cause easier to reproduce and fix:

  • #47391
  • #47804
  • #42256
  • #44735
  • #36511
  • #24387

View original on GitHub ↗

8 Comments

jdinino · 4 months ago

Confirming another repro with concrete data. File 20a.jpg on disk was a 100KB HTML error page (likely a failed download saved with the wrong extension). When Read ran on it, Claude Code embedded the bytes as image/png in the tool_result, which then poisoned the session transcript — every subsequent turn replayed the bad block and got a 400 Could not process image.

Workaround that recovered the session without losing history:

  1. Locate the session JSONL in ~/.claude/projects/<project>/
  2. Validate each image block's base64 against the declared media_type magic bytes (PNG: 89 50 4E 47, JPEG: FF D8 FF, WEBP: RIFF, GIF: GIF8)
  3. Replace mismatched blocks with a text block placeholder so the rest of the transcript stays intact

Fix ideas for the Claude Code side:

  • Sniff magic bytes before wrapping Read output as image/*; fall back to text if bytes don't match the extension
  • When a 400 Could not process image is returned, auto-quarantine offending blocks from the replayed transcript instead of hard-failing the session
uraomotedo · 4 months ago

+1, hit the same class of bug today on Claude Code with a different mislabeling shape — worth recording because it shows the issue isn't specific to "JSON-in-png", it's any non-PNG bytes behind a .png extension.

Repro

cp /tmp/something.bmp /tmp/preview.png    # rename only, contents still BMP
# Read("/tmp/preview.png")

Result:

API Error: 400 {"type":"error","error":{"type":"invalid_request_error",
  "message":"messages.55.content.36.image.source.base64.data: Image format image/png not supported"}}

After that every subsequent turn re-sends the same payload and gets the same 400. Session is dead until rewound.

Why this matters beyond the original report

The original issue showed JSON-text-in-.png. My case is actual image bytes (BMP) in .png — i.e. the file is an image, just not the format the extension claims. So the validation can't just be "is this a PNG / JPEG / GIF / WebP magic number" on the byte stream — but that's exactly the check that would have caught both cases.

A magic-number sniff in the Read tool before emitting an image content block (and falling back to a text/error block when it doesn't match a supported format) would fix the whole family.

Workaround I'm now using

Added a project-level rule: file <path> first, never cp a.bmp b.png, and normalize via PIL before Read:

python3 -c "from PIL import Image; Image.open('src.bmp').save('dst.png')"

But yeah, +1 on "the recovery side is the actual bug" — input validation should prevent it, but a single bad attachment poisoning the rest of the session is the part that turns a small mistake into a session loss.

justi · 4 months ago

@eltmon @jdinino @uraomotedo — the mislabeled-image class (ICO/HTML/other as .png/.jpg) you all hit is exactly what this hook mitigates. The subprocess proxy intercepts every image Read before it reaches the main session; invalid or mismatched formats fail inside a sandboxed claude --model haiku call and return text only — no malformed bytes ever touch the primary context, no thinking-block corruption, session stays alive.

Gist: https://gist.github.com/justi/8265b84e70e8204a8e01dc9f99b8f1d0

mtschoen · 4 months ago

+1, another repro on Claude Code 2.1.113 with a new mislabeling shape: stderr output from a screenshot CLI written to a .png file.

What happened

A file at docs/images/fold-current.png (14278 bytes) was created by a Windows screenshot CLI that wrote its warning text to the target path instead of PNG bytes — something like screenshot-cli > fold-current.png where the tool printed to stdout on error. Magic bytes on disk were 5b 57 61 72 6e 69 6e 67 ([Warning), not \x89 50 4e 47.

When Read was called on that path, Claude Code wrapped the bytes as image/png and sent them to the API. Result:

API Error: 400 {"type":"error","error":{"type":"invalid_request_error",
  "message":"Could not process image"},"request_id":"req_011CaF4QdxoYd7v9dwe77E9t"}

Session wedged — every subsequent prompt replayed the same tool_result and got the same 400.

Decoded preview of what got sent as image/png

[Warning] Multiple displays were found, but no display id was specified!
Defaulting to the first display found, however this default is not guaranteed…

So the "PNG" was a 14 KB ASCII error message. The extension was the only reason it was classified as an image.

Recovery

Used the same transcript-surgery approach @jdinino described — located ~/.claude/projects/<slug>/<session-id>.jsonl, found the single line containing the "type":"image" tool_result, replaced the image content item with a text stub (preserving tool_use_id so the pair stays matched), and also scrubbed the internal toolUseResult.file.base64 on the same line for cleanliness. claude --resume <session-id> after that unwedged the session without losing the preceding ~1000 turns.

Why this shape matters

The three earlier comments cover JSON/HTML/BMP-as-PNG — all "structured bytes with the wrong extension." This one is different: the file wasn't a format at all, it was a command's stderr output that happened to land at a .png path. It's a very common Windows/Unix pattern (tool writes error to stdout/stderr, user redirected to a file) and it produces the exact same unrecoverable state.

Both of the OP's suggested fixes would catch it:

  1. Magic-byte validation in Read before emitting an image block (would see 5b 57 61 72…, fall back to text)
  2. Drop the offending block from context on API 400 for image (would at least stop the poison-every-turn loop)
stolsvik · 4 months ago

_"After that, the bad image stays in context and every subsequent turn fails with the same error. The session becomes unusable until I rewind the session history."_

Ah, THANKS for that insight. Makes total sense, and should be pretty fixable by the Claude Code folks?!

I had Claude harden my "Remote Control and Screenshotter" Java/Swing plugin, and also the script that invokes the HTTP call: The "RC server" validates that the file is >100 bytes, and that the first few bytes are correct magic, and then the shell side (rc.sh) runs file over it, identifying the type and validates that towards expected file ending - and file also "half loads" the image, producing e.g. "/tmp/mn-cur.png: PNG image data, 1200 x 720, 8-bit/color RGBA, non-interlaced" as output. If this fails validation, the script renames the file and produces an error output and exits non-zero - all so that the Claude instance doesn't just blindly tries to Read the file and end up in this situation.

Claude Code should do something similar: Actually load the image and ensure that it actually is an image file matching the file ending - before dumping it into context.

There could also be some belt-and-suspenders logic of chopping off that last message if the error 400 comes back from the server - so that one doesn't end up in this pretty hard-core, close-to-irreparable failure mode. I had to use /rewind and my only options were all of my "please try again" messages (all of which obviously just fails again), and then one WAY earlier in the transcript (choose "don't rewind files" in the options, and tell Claude what happened - it quite gracefully recovered..).

ozz-wizard · 3 months ago

Confirming this independently from production — same root cause, different file content (SVG saved with .png extension).

Repro on our side

In an autonomous-agent setup (Paperclip's Claude adapter), an agent ran:

curl -o ./output/character-roughs-sheet.png https://example.com/path/to/attachment   # the URL actually served image/svg+xml

…then later did Read("./output/character-roughs-sheet.png") in the same Claude session. The SDK trusted the .png extension, packed the file as a base64 image/png content block, and persisted it into the resumed session jsonl (~/.claude/projects/.../<session-id>.jsonl).

From that turn forward, every wake of that session sent the same poisoned block back to the API and got:

400 invalid_request_error: Could not process image

…before the agent ever got a turn. The block stayed in the jsonl, so re-running the task didn't help. Only manual quarantine of the session jsonl (mv <id>.jsonl <id>.jsonl.poisoned-<run-id>) plus deletion of the local artifact unstuck it.

Blast radius (why this matters beyond the single session)

In an autonomous setup the failure isn't just "session unusable" — it cascades:

  • 1 poisoned session → 4 stranded-task recovery jobs spawned by our supervisor (it observed the agent never producing output and treated each wake as a stranded run).
  • Re-arming the issue, replacing source files, resolving blockers — none of it helped, because the poison lives in the SDK's session jsonl, not in any user-visible context.
  • The supervisor / resume layer has no signal that the session is permanently broken vs. transiently failing — so it retry-loops indefinitely.

Fix priorities (echoing #47976 + adjacent issues)

Two surfaces, both worth fixing:

  1. Validate magic bytes vs declared media_type at persist time in the Read tool (and any other path that converts a file into an image/* content block — Bash stdout, MCP tool-results). On mismatch, substitute a text marker like [image stripped: declared image/png, actual content image/svg+xml — original at <path>] instead of the image block. This prevents poisoning at the source.
  2. Recover from a 400 Could not process image by stripping the offending image block from the persisted jsonl (or rotating the session with a forensic snapshot) instead of looping forever. Without (2), already-poisoned sessions in the wild stay broken even after (1) ships.

Related dupes / adjacent reports we found while triaging this internally: #13396, #39146, #44735, #47391, #47804, #28684, #53901, #56898, #11936, #15807. The class of bug is well-known in the tracker; this comment is just adding one more concrete production data point — autonomous-agent setup, SVG-as-PNG, multi-session blast radius — in case that helps prioritize.

Happy to share the quarantined jsonl (with sensitive tool output redacted) if it'd help reproduce.

claude[bot] contributor · 3 months ago

This issue was fixed as of version 2.1.144.

github-actions[bot] · 1 month ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.