Read trusts file extension: a non-image file named *.png permanently bricks the session (400)
Environment
- Claude Code v2.1.89
- Windows 11 Pro for Workstations (10.0.22631)
- Model: claude-sonnet-4-6
Summary
When Read is pointed at a file whose extension says image but whose bytes are not an image (e.g. a PDF named foo.png), the tool surfaces it as an image and attaches it to message.content with a media_type derived from the extension. The API correctly rejects it, and from that point on every request in the session fails, because the bad attachment is replayed from history on every turn.
Steps to reproduce
- Create a non-image file that has an image extension. PyMuPDF does this by accident very easily, because
Document.save()writes a PDF regardless of the extension:
``bash`
python -c "import fitz; d=fitz.open(); d.new_page(); d.save('x.png')"
x.png
now begins with the bytes %PDF-1.7`.
- In Claude Code:
Read x.png
- The tool output is
Read image (N KB)— no warning, no validation error.
- Send any subsequent message (including
/continue).
Actual
API Error: 400 {"error":{"message":"messages.N.content.0.tool_result.image[0]:
You have uploaded an unsupported image. Please make sure your image is valid and
has one of the following formats: webp, png, jpeg, and gif.", ...}}
Every retry fails identically, because the offending attachment lives in history and is re-sent each turn.
Expected
Read should validate the file's magic bytes before treating it as an image. If the bytes don't match the claimed type, it should refuse to attach and report something like x.png is not a valid image (found PDF header) — at minimum, warn.
Impact
The session becomes unusable and there is no in-product recovery path:
/continueretries the same in-memory request.- The error names
messages.N(an API array index), not the offending file, so the user can't tell which attachment broke it. - The only known fix is editing the session JSONL on disk to replace the bad
imageblock, then--resume. That is not something a normal user should have to do.
Suggested fixes
- Sniff bytes, not extensions. Before emitting an image block, verify the magic number (
\x89PNG,\xFF\xD8\xFF,RIFF…WEBP,GIF8); on mismatch, return a normal text error instead of attaching. - Make the failure recoverable and legible. Name the offending file in the error surfaced to the user, and offer a way to drop the last attachment from history so the session isn't bricked.
Notes
The same bytes are also stored in the transcript's local-only toolUseResult field as {"type":"image","file":{"base64":...,"type":"image/png","originalSize":...}} — i.e. it is labeled image/png purely from the extension, which points at (1) as the root cause.