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
.png file (e.g., some_command > screenshot.png where 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"}}
[/+][-]
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:
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 with GIF8, WebP with RIFF....WEBP
- Reject files that don't match — if a
.png file 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-images command 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>/
The most recently modified .jsonl file is your current conversation.
-
Back it up:
cp <file>.jsonl <file>.jsonl.bak
-
Run this script to strip image blocks while preserving all text context:
#!/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:
Environment
- Claude Code CLI v2.1.37
- macOS (Darwin 25.0.0)
- Affects all platforms (macOS, Windows, Linux) based on related issues
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/.jpgfile gets sent to the API as an "image", which the API rightfully rejects.Common real-world triggers:
.pngfile (e.g.,some_command > screenshot.pngwhere the command outputs error text instead of image data).jpgExample:
utmctl file pullfails withOSStatus error -2700but the error text gets redirected into a.pngfile. Claude Code sees the.pngextension, base64-encodes the 173-byte error string as an image, and the API returns: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:
Suggested Fix
Prevention: Validate images before adding to context
Before base64-encoding a file as an image, check the actual file content:
\x89PNG\r\n\x1a\n, JPEG with\xFF\xD8\xFF, GIF withGIF8, WebP withRIFF....WEBP.pngfile doesn't have PNG magic bytes, read it as text or skip it with a warningRecovery: Handle the error gracefully when it does occur
When the API returns
400with"Could not process image":UX improvement
/strip-imagescommand to remove all image blocks from the current conversationWorkaround
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:
The most recently modified
.jsonlfile is your current conversation.Back it up:
Run this script to strip image blocks while preserving all text context:
Run it:
Resume the session:
Environment