Skip to content

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

@robinbraemer

Description

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

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:

  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.

  2. Back it up:

    cp <file>.jsonl <file>.jsonl.bak
    
  3. 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")
  4. Run it:

    python3 fix.py <file>.jsonl.bak <file>.jsonl
    
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions