Corrupted image in context permanently breaks session — workaround and suggested fix
Problem
When Claude Code reads a file with an image extension (.png, .jpg, etc.) that does not contain valid image data, it base64-encodes the content and adds it to the conversation context as an image block. The API then rejects it with a 400 error, but the invalid image remains in the stored conversation JSONL — causing every subsequent message to fail in a loop.
There is no image validation before adding to context, and no recovery mechanism when this error occurs.
Root Cause
Claude Code trusts the file extension to determine if a file is an image. It does not validate the actual file content (magic bytes, format headers) before base64-encoding it into the conversation. This means any non-image data in a .png/.jpg file gets sent to the API as an "image", which the API rightfully rejects.
Common real-world triggers:
- A shell command fails but writes its error message to a
.pngfile (e.g.,some_command > screenshot.pngwhere the command outputs error text instead of image data) - A failed HTTP download saves a 404 HTML page as
.jpg - A file transfer from a VM fails, writing an OS error string to the output file
- A truncated or partially written screenshot file
Example: utmctl file pull fails with OSStatus error -2700 but the error text gets redirected into a .png file. Claude Code sees the .png extension, base64-encodes the 173-byte error string as an image, and the API returns:
API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"Could not process image"}}
<img width="1125" height="153" alt="Image" src="https://github.com/user-attachments/assets/9f3811a2-3fed-4fab-80a2-8b364f8e517d" />[/+][-]
Every subsequent message in the session then fails with the same error because the invalid image block is re-sent with every API call.
Related Issues
This is a widespread problem with many reports:
- #4083 — Image Processing Failure: API Rejects Unrelated Context Image (10 comments)
- #19031 — Corrupted image in context causes persistent API 400 errors
- #22351 — API Error: 400 "Could not process image"
- #12616 — API Error: 400 "Could not process image"
- #13594 — Could not process image (compact system + Playwright interaction)
- #11975 — Anthropic API Error: Image does not match provided media type
Suggested Fix
Prevention: Validate images before adding to context
Before base64-encoding a file as an image, check the actual file content:
- Verify magic bytes — PNG starts with
\x89PNG\r\n\x1a\n, JPEG with\xFF\xD8\xFF, GIF withGIF8, WebP withRIFF....WEBP - Reject files that don't match — if a
.pngfile doesn't have PNG magic bytes, read it as text or skip it with a warning - Check minimum file size — a valid image is at least a few hundred bytes; a 173-byte "PNG" is clearly not an image
Recovery: Handle the error gracefully when it does occur
When the API returns 400 with "Could not process image":
- Catch the error and identify image content blocks in the conversation
- Strip or replace the offending image block(s) with a text placeholder
- Retry the API call automatically
- Or at minimum, prompt the user: "An image in context is causing errors. Remove it and retry? [Y/n]"
UX improvement
- Add a
/strip-imagescommand to remove all image blocks from the current conversation - Show the actual file content when image validation fails (e.g., "Warning: /tmp/screenshot.png is not a valid PNG — contains text: 'Error from event: ...'")
Workaround
Until this is fixed, users can manually strip images from the conversation JSONL file to recover their session without losing text context:
- Find the conversation file:
```
ls -lt ~/.claude/projects/<your-project-path>/
.jsonl` file is your current conversation.
The most recently modified
- Back it up:
````
cp <file>.jsonl <file>.jsonl.bak
- Run this script to strip image blocks while preserving all text context:
```python
#!/usr/bin/env python3
"""Strip image content blocks from a Claude Code conversation JSONL file."""
import json, sys, os
def strip_images(content):
if isinstance(content, list):
return [strip_images(item) for item in content if item is not None]
elif isinstance(content, dict):
if content.get("type") == "image":
return {"type": "text", "text": "[image removed to fix conversation]"}
if content.get("type") == "tool_result" and isinstance(content.get("content"), list):
content["content"] = strip_images(content["content"])
if "message" in content and isinstance(content["message"], dict):
msg = content["message"]
if "content" in msg:
msg["content"] = strip_images(msg["content"])
return content
return content
infile, outfile = sys.argv[1], sys.argv[2]
images_removed = 0
with open(infile) as f_in, open(outfile, 'w') as f_out:
for line in f_in:
line = line.strip()
if not line:
continue
obj = json.loads(line)
before = json.dumps(obj)
obj = strip_images(obj)
if json.dumps(obj) != before:
images_removed += 1
f_out.write(json.dumps(obj) + '\n')
print(f"Removed images from {images_removed} lines")
print(f"Size: {os.path.getsize(infile)/1024/1024:.1f}MB -> {os.path.getsize(outfile)/1024/1024:.1f}MB")
```
- Run it:
````
python3 fix.py <file>.jsonl.bak <file>.jsonl
- Resume the session:
````
claude --resume
Environment
- Claude Code CLI v2.1.37
- macOS (Darwin 25.0.0)
- Affects all platforms (macOS, Windows, Linux) based on related issues
16 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Adding: this also occasionally happens when pasting a macOS clipboard screenshot to CC in the Desktop app (which shares the same storage files).
Same issue here, but triggered by a GIF image on an external website.
When Claude Code fetches a webpage (https://cube-in.cz) that contains a
GIF logo (logo-cube-in.gif), it attempts to process it as an image and
the API returns 400 "Could not process image". The session then breaks
permanently until /clear.
GIF is not a supported format (supported: JPEG, PNG, WebP, AVIF), but
there is no validation before sending to the API and no recovery after
the 400 error.
The fix suggested in this issue (magic byte validation + strip offending
image block on 400) would also cover this case.
Duplicates of this bug have been getting reported for over 6 months now. I understand there may be more important problems, but considering this completely kills the Claude process, how difficult is it to implement a simple is_image() check before baking an image file into the session? I feel like the dumbest AI coding model could fix this bug in a couple minutes if you'd just stop auto-closing all these bug reports about it.
Bump to avoid auto-close.
Was this fixed for you with the 2.1.71 release @robinbraemer?
Another real-world trigger for this: Git LFS pointer files.
Files tracked by Git LFS have image extensions (
.png,.jpg) but contain a ~130-byte text pointer:If you clone a repo without LFS installed (or
git lfs pullhasn't run), every image file is one of these pointers. The Read tool sees the.pngextension, base64-encodes the pointer text as an image, and the session enters the same unrecoverable error loop described here.Reproduction:
Filed as #32764 separately before finding this issue — closing that as a duplicate pointing here.
Root Cause (v2.1.76)
The image reading pipeline has no bail-out for non-image content. Three functions form the chain:
1.
pF6(magic byte detection) — defaults to"image/png"when bytes don't match any known format, instead of returningnull:2.
XV8(Read tool image handler) — whensharpcorrectly rejects the content ("unsupported image format"), the catch block wraps the raw bytes as PNG anyway:3.
rf4(paste-time handler) — callssharpwith no catch block at all, crashing the input handler when a bad image propagates via copy-paste.Minimal Patch
Tested on a stock v2.1.76 devcontainer (no MCP, no custom config). Apply to
cli.js:Breaks on next
npm update— meant as a stopgap, not a permanent solution.Full write-up with both vectors (Read tool + paste propagation): https://github.com/tfvchow/field-notes-public/issues/55
Additional trigger: structurally valid PNG with corrupt pixel data
Adding another real-world reproduction case that's worth noting because it would bypass the suggested magic-byte validation fix.
Scenario: A PNG file (5.9KB) has a valid PNG header and valid metadata (268×154, RGBA, sRGB, gamma). PIL/Pillow can read the header successfully. However, the compressed pixel data stream is corrupt —
Image.load()raisesOSError: broken data stream when reading image file.Claude Code's Read tool reads the file, base64-encodes it (valid PNG magic bytes, valid IHDR chunk), and adds it to conversation context. The API then returns:
From that point, every subsequent message fails with the same error — the session is permanently broken until restarted.
Why this matters for the fix: The suggested magic-byte check (#24387) would not catch this case, since the file has valid PNG magic bytes (
\x89PNG\r\n\x1a\n) and a valid IHDR chunk. The corruption is in the IDAT (pixel data) chunks. To catch this class of corruption, the validation would need to either:Image.open().load()orImage.verify()) — not just check magic bytes400 "Could not process image"response, strip the offending image block from context, and retryEnvironment: Claude Code on Linux (WSL2), Claude Opus 4.6
Reproduction:
Workaround: PreToolUse hook with subprocess proxy — unlimited images per session
Until this is fixed upstream, here's a hook that completely eliminates the "Could not process image" error by keeping image data out of the conversation context entirely.
How it works
Instead of passing image data to the main context (which accumulates and crashes after ~8 images), the hook:
claude --model haikusubprocess with a fresh context to analyze itPostToolUsehook cleans up temp files automaticallyYou can read hundreds of images in a single session without hitting any limits.
Features (v9)
touch /tmp/claude-image-directwhen you need raw image in contextInstall
Add to
~/.claude/settings.json— full config with cleanup hook in the gist README.Gist: https://gist.github.com/justi/8265b84e70e8204a8e01dc9f99b8f1d0
Stress-tested: 15 images (PNG + JPG, 313B–240KB) in one session, zero errors.
---
cc @robinbraemer @1-fares @NickSdot @chrisspen @mrtysn @pytelCZ @socketbox @tfvchow — updated workaround, now with subprocess proxy (unlimited images per session).
Confirming the original
fix.pyworkaround worked for me. Thanks! 🙌🏻In my case, the issue specifically occurred with a
.HEICimage —.jpgand.pngwork fine.💡 Session Recovery Workaround (No ffmpeg needed)
If your session is bricked by an oversized image, here's a clean recovery method that preserves your full context:
~/.claude/projects/(orC:\Users\<You>\.claude\projects\on Windows).jsonlfile (by modified date)/initto re-initialize project context.jsonlfile into the chat and tell Claude: "Analyze this session log and get fully caught up on where we left off."This works because the
.jsonlfiles contain the complete conversation history. Feeding it into a fresh session gives you all the context without the corrupted state (the stuck oversized image causing the 400 error loop).Having a similar issue want this fixed without need for a workaround for future ref
Adding a related repro from #53228: a URL→404 vector — the model
curl's an image URL, gets an HTML 404 page saved asg-test.png, thenReads it as an image, bricking the session. Same root cause (no magic-byte validation, no 400 recovery). The thread there proposes the same dual fix you call out: validate before send + drop-and-retry on 400. Closing mine in favor of this one.I faced this issue twice and managed to escape it preserving the context. Both times happened when it needed to pull design from figma via figma plugin, iirc it tried to make and read screenshots from figma.
First time I got through it using
/rewind, I rolled back to the state before last request and it went well.Second time it happened right after autocompact, what technically cleared the conversation and
/rewindoptions. To get the compacted context I ranclaude -rand previewed the conversation, there the compated prompt is shown, and I copied it and pasted into new empty session. That way it wasn't stuck anymore.Leaving this in hope it may help someone.