Corrupted image in context permanently breaks session — workaround and suggested fix

Open 💬 16 comments Opened Feb 9, 2026 by robinbraemer

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"}}

<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:

  1. Verify magic bytes — PNG starts with \x89PNG\r\n\x1a\n, JPEG with \xFF\xD8\xFF, GIF with GIF8, WebP with RIFF....WEBP
  2. 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
  3. 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":

  1. Catch the error and identify image content blocks in the conversation
  2. Strip or replace the offending image block(s) with a text placeholder
  3. Retry the API call automatically
  4. 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:

  1. Find the conversation file:

``
ls -lt ~/.claude/projects/<your-project-path>/
`
The most recently modified
.jsonl` file is your current conversation.

  1. Back it up:

``
cp <file>.jsonl <file>.jsonl.bak
``

  1. 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")
```

  1. Run it:

``
python3 fix.py <file>.jsonl.bak <file>.jsonl
``

  1. 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

View original on GitHub ↗

16 Comments

github-actions[bot] · 5 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/19031
  2. https://github.com/anthropics/claude-code/issues/17452
  3. https://github.com/anthropics/claude-code/issues/15004

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

NickSdot · 5 months ago

Adding: this also occasionally happens when pasting a macOS clipboard screenshot to CC in the Desktop app (which shares the same storage files).

pytelCZ · 4 months ago

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.

chrisspen · 4 months ago

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.

NickSdot · 4 months ago

Bump to avoid auto-close.

NickSdot · 4 months ago

Was this fixed for you with the 2.1.71 release @robinbraemer?

mrtysn · 4 months ago

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:

version https://git-lfs.github.com/spec/v1
oid sha256:796e62f8e6d70b18cbe2fcda3debc29ffce327eaa12bbdc3650fef6d6189bba4
size 914901

If you clone a repo without LFS installed (or git lfs pull hasn't run), every image file is one of these pointers. The Read tool sees the .png extension, base64-encodes the pointer text as an image, and the session enters the same unrecoverable error loop described here.

Reproduction:

curl -sL "https://raw.githubusercontent.com/chickensoft-games/Chicken/main/icon.png" -o /tmp/test.png
# Read /tmp/test.png in Claude Code → session poisoned

Filed as #32764 separately before finding this issue — closing that as a duplicate pointing here.

tfvchow · 4 months ago
Note: This root cause analysis was done collaboratively with Claude Code (Opus 4.6). If that bothers you, feel free to scroll past. Nobody's getting paid here — let alone using my own subscription to troubleshoot for Anthropic. Just sharing what I found in case it helps.

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 returning null:

// after checking PNG, JPEG, GIF, WebP magic bytes:
return "image/png";  // ← HTML, error text, anything unknown = "PNG"

2. XV8 (Read tool image handler) — when sharp correctly rejects the content ("unsupported image format"), the catch block wraps the raw bytes as PNG anyway:

try {
  O = await Bk(Y, z, w);    // sharp rejects
} catch(H) {
  O = bP1(Y, w, z);         // fallback: base64-encode raw bytes, label as PNG, send to API
}

3. rf4 (paste-time handler) — calls sharp with 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:

# python3 patch.py $(which claude | xargs readlink -f | xargs dirname)/cli.js
import sys
with open(sys.argv[1], 'r') as f: content = f.read()

# 1. pF6: return null for unknown magic bytes
content = content.replace(
    'return"image/webp"}return"image/png"}',
    'return"image/webp"}return null}', 1)

# 2. XV8: bail out when pF6 returns null
content = content.replace(
    'let _=pF6(Y),w=_.split("/")',
    'let _=pF6(Y);if(_===null)return{type:"text",file:{filePath:A,content:"[Not a valid image file]",numLines:0,startLine:1,totalLines:0}};let w=_.split("/")', 1)

# 3. rf4: skip non-images before sharp
content = content.replace(
    'let z=D8Y(K).slice(1).toLowerCase()||"png",_=await Bk(Y,Y.length,z)',
    'let z=D8Y(K).slice(1).toLowerCase()||"png";if(pF6(Y)===null)return null;let _=await Bk(Y,Y.length,z)', 1)

with open(sys.argv[1], 'w') as f:
    f.write(content)

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

1-fares · 3 months ago

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() raises OSError: 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:

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

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:

  1. Actually decode the image data (e.g., PIL Image.open().load() or Image.verify()) — not just check magic bytes
  2. Or, more practically, implement the recovery path: catch the 400 "Could not process image" response, strip the offending image block from context, and retry

Environment: Claude Code on Linux (WSL2), Claude Opus 4.6

Reproduction:

$ file image.png
image.png: PNG image data, 268 x 154, 8-bit/color RGBA, non-interlaced
$ ls -la image.png
-rw-r--r-- 1 user user 5948 image.png
$ python3 -c "from PIL import Image; img = Image.open('image.png'); img.load()"
OSError: broken data stream when reading image file
socketbox · 3 months ago

❯ Could you please validate the image at /home/chbfoo/Pictures/Screenshots/sso_fail.png? Make sure it's structurally valid, with a non-corrupted
pixel data stream.

● Bash(file /home/chbfoo/Pictures/Screenshots/sso_fail_ccullen.png && python3 -c "
      from PIL import Image
      import sys
      try:
          img = Image.open('/home/chbfoo/Pictures/Screenshots/sso_fail_ccullen.png')
          img.verify()
          print('verify() passed')
      except Exception as e:
          print(f'verify() failed: {e}')

      try:
          img2 = Image.open('/home/chbfoo/Pictures/Screenshots/sso_fail_ccullen.png')
          img2.load()
          print(f'load() passed — size={img2.size}, mode={img2.mode}')
      except Exception as e:
          print(f'load() failed: {e}')
      ")
  ⎿  /home/chbfoo/Pictures/Screenshots/sso_fail.png: PNG image data, 772 x 970, 8-bit/color RGB, non-interlaced
     verify() passed
     load() passed — size=(772, 970), mode=RGB

                                                                                                                           10:56 AM claude-sonnet-4-6
● The image is structurally valid:

  - Format: PNG, 772×970 px, 8-bit RGB, non-interlaced
  - verify(): Passed — PNG chunks and CRC checksums are intact
  - load(): Passed — full pixel data stream decompressed without errors

  No corruption detected.

❯ What's the image show?
  ⎿  [Image #1]

                                                                                                                                 10:57 AM <synthetic>
  ⎿  API Error: 400 Could not process image

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  Showing detailed transcript · ctrl+o to toggle · ctrl+e to show all
justi · 3 months ago

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:

  1. Converts the image to a safe JPEG (max 800px, q70, flattens transparency)
  2. Spawns a new claude --model haiku subprocess with a fresh context to analyze it
  3. Returns a text-only description — zero image data in the main context
  4. A PostToolUse hook cleans up temp files automatically

You can read hundreds of images in a single session without hitting any limits.

Features (v9)

  • Proxy mode (default) — subprocess analyzes, returns text
  • Auto-context — reads session transcript to understand what the user is asking about
  • Direct modetouch /tmp/claude-image-direct when you need raw image in context
  • Fallback — if subprocess fails, passes converted JPEG directly
  • Re-entry guard — prevents infinite loops on temp files
  • Prompt injection resistant — subprocess ignores file paths from conversation context
  • PostToolUse cleanup — temp files deleted after Read completes

Install

mkdir -p ~/.claude/hooks
curl -o ~/.claude/hooks/png-safe-read.sh \
  'https://gist.githubusercontent.com/justi/8265b84e70e8204a8e01dc9f99b8f1d0/raw/png-safe-read.sh'
chmod +x ~/.claude/hooks/png-safe-read.sh

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).

space-fwog · 3 months ago

Confirming the original fix.py workaround worked for me. Thanks! 🙌🏻

In my case, the issue specifically occurred with a .HEIC image — .jpg and .png work fine.

QuicksilverSlick · 3 months ago

💡 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:

  1. Go to ~/.claude/projects/ (or C:\Users\<You>\.claude\projects\ on Windows)
  2. Find your project folder and locate the most recent .jsonl file (by modified date)
  3. Start a fresh session in your project directory
  4. Run /init to re-initialize project context
  5. Drag the .jsonl file into the chat and tell Claude: "Analyze this session log and get fully caught up on where we left off."

This works because the .jsonl files 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).

Full write-up with details: https://github.com/anthropics/claude-code/issues/13480#issuecomment-4237000378
kutzki · 2 months ago

Having a similar issue want this fixed without need for a workaround for future ref

router0mail · 2 months ago

Adding a related repro from #53228: a URL→404 vector — the model curl's an image URL, gets an HTML 404 page saved as g-test.png, then Reads 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.

HiWord9 · 2 months ago

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 /rewind options. To get the compacted context I ran claude -r and 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.