[Bug] Read on mislabeled .png causes unrecoverable 'Could not process image' session corruption
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:
- Input validation failure —
Readshould verify that a.pngfile is actually a PNG before emitting an image block - 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
- Create a file named something like
foo.png - Put JSON text into it instead of PNG bytes, for example:
{"success":true,"format":"png","data":"iVBORw0KGgoAAAANSUhEUgAA..."}
- Ask Claude Code to
Readthat file - Claude Code emits an image content block
- Anthropic API returns:
API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"Could not process image"}}
- Send any follow-up message
- The same error repeats on every turn; session is effectively bricked
Expected Behavior
Claude Code should do one or more of:
- Inspect file signatures / MIME instead of trusting the filename extension alone
- If the file is not a real image, return text/metadata instead of image content
- If the API rejects an image block, remove that image from context for subsequent turns
- Allow
/compact,/resume, or normal follow-up prompts to proceed after surfacing the failure
Actual Behavior
Readon the mislabeled.pngcaused 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
Readon that path - the
Readtool result contained an image block whose base64 payload decoded to JSON, not raw PNG bytes - decoding that outer JSON's
datafield produced a valid PNG - local inspection of the file with
filereported: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
Readtool
Suggested Fix
For Read on image-like paths:
- Validate magic bytes / MIME before constructing an image content block
- If file contents are text/JSON, treat them as text even if the extension is
.png - 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
8 Comments
Confirming another repro with concrete data. File
20a.jpgon disk was a 100KB HTML error page (likely a failed download saved with the wrong extension). WhenReadran on it, Claude Code embedded the bytes asimage/pngin the tool_result, which then poisoned the session transcript — every subsequent turn replayed the bad block and got a 400Could not process image.Workaround that recovered the session without losing history:
~/.claude/projects/<project>/imageblock's base64 against the declaredmedia_typemagic bytes (PNG:89 50 4E 47, JPEG:FF D8 FF, WEBP:RIFF, GIF:GIF8)textblock placeholder so the rest of the transcript stays intactFix ideas for the Claude Code side:
Readoutput asimage/*; fall back to text if bytes don't match the extensionCould not process imageis returned, auto-quarantine offending blocks from the replayed transcript instead of hard-failing the session+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
.pngextension.Repro
Result:
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
Readtool before emitting animagecontent 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, nevercp a.bmp b.png, and normalize via PIL before Read: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.
@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 haikucall 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
+1, another repro on Claude Code
2.1.113with a new mislabeling shape: stderr output from a screenshot CLI written to a.pngfile.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 likescreenshot-cli > fold-current.pngwhere the tool printed to stdout on error. Magic bytes on disk were5b 57 61 72 6e 69 6e 67([Warning), not\x89 50 4e 47.When
Readwas called on that path, Claude Code wrapped the bytes asimage/pngand sent them to the API. Result:Session wedged — every subsequent prompt replayed the same tool_result and got the same 400.
Decoded preview of what got sent as image/png
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 (preservingtool_use_idso the pair stays matched), and also scrubbed the internaltoolUseResult.file.base64on 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
.pngpath. 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:
Readbefore emitting an image block (would see5b 57 61 72…, fall back to text)_"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
fileover 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 toReadthe 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..).
Confirming this independently from production — same root cause, different file content (SVG saved with
.pngextension).Repro on our side
In an autonomous-agent setup (Paperclip's Claude adapter), an agent ran:
…then later did
Read("./output/character-roughs-sheet.png")in the same Claude session. The SDK trusted the.pngextension, packed the file as a base64image/pngcontent 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:
…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:
Fix priorities (echoing #47976 + adjacent issues)
Two surfaces, both worth fixing:
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.400 Could not process imageby 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.
This issue was fixed as of version 2.1.144.
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.