How to Rescue Claude Code Sessions Stuck on Image Dimension Limits
Recently I hit a confusing error in Claude Code:
An image in the conversation exceeds the dimension limit for many-image requests (2000px). Start a new session with fewer images.
The screenshot I’d just pasted was completely normal—just 100×200 pixels. By intuition, this should mean “the last image is oversized,” but it wasn’t. This is a cumulative bug with counterintuitive trigger conditions, root causes, and fixes. Once you hit it, you keep hitting it. This post explains the rules, the mechanics, and ready-to-use tooling to fix it.
The Symptom
A typical trigger sequence:
- You’re halfway through a Claude Code session, and you’ve pasted a few screenshots along the way (say one is 2500×1800—Retina screenshots easily exceed 2000 px).
- You keep chatting and paste a new screenshot (doesn’t matter how large).
- That request fails instantly with the dimension limit error.
- After that, even plain-text-only messages fail. The session is completely stuck. Your only option is to start a new one.
If you’re using Paseo (a client that aggregates multiple Claude Code / Codex tabs), one dead tab doesn’t break the others, but that tab’s conversation context is essentially wasted.
Root Cause: Anthropic API’s Multi-Image Tiering Rules
Anthropic’s Messages API has two tiers for image size limits:
| Images in the request | Max size per image |
|---|---|
| 1 image (single-image mode) | Up to 8000×8000 |
| ≥ 2 images (multi-image mode) | Each edge must be ≤ 2000 px |
The key phrase is “images in the request,” not “images in this message.” Claude Code (and any compatible client) works like this: each request resends the entire conversation history—including all historical images—as context. So:
- You paste one 2500×1800 image. Single-image mode. OK.
- You paste any other image a second time, even a 50×50 emoji screenshot. Now the request contains 2 images, multi-image mode triggers. The API retrospectively checks the history, sees that first 2500-wide image violates the 2000 px rule, and rejects the entire request.
- After that, even if you send only text, those 2 images remain in the context history, and requests keep failing.
So it’s not “the latest image is the problem”—it’s “from the 2nd image onward, any historical image exceeding 2000 px breaks the entire session.”
Retina display screenshots are naturally oversized: on a MacBook with 3× scaling, a window screenshot displayed at 800×600 pixels is actually 2400×1800 pixels—over the line.
Basic Fix: Strip Base64 Images from the JSONL
Claude Code stores each session as JSON Lines: ~/.claude/projects/<project-slug>/<session-uuid>.jsonl. Each line is a message, and images are embedded as base64, like:
{
"role": "user",
"content": [
{ "type": "text", "text": "Check out this error" },
{ "type": "image", "source": { "type": "base64", "data": "iVBORw0...very long base64..." } }
]
}
The rescue approach is simple: replace source with a placeholder. The base64 is no longer embedded in the context, and the multi-image rule no longer triggers. Your text prompt (“Check out this error”) and Claude’s text interpretation of the image (“The screenshot shows a 404 error…”) are both preserved exactly. The conversation semantics continue; you only lose the ability to scroll back and view the image itself.
The existing tool for this is ianbmacdonald/claude-session-rescue. It’s a pure Python script with atomic writes and automatic .bak backups:
git clone https://github.com/ianbmacdonald/claude-session-rescue ~/tools/claude-session-rescue
# Dry-run to see what will change
python3 ~/tools/claude-session-rescue/scripts/session_rescue.py \
--session-id abc12345 --dry-run
# Actually run it
python3 ~/tools/claude-session-rescue/scripts/session_rescue.py \
--session-id abc12345
After running it, you can go back to Claude Code and send messages normally.
Two Real Pain Points I Hit
After fixing it the first time with that basic script, I ran into two more issues:
Pain Point 1: I Don’t Know Which Paseo Tab Maps to Which sessionId
I’m used to giving Paseo tabs meaningful names (organized by project domain), but the rescue script needs the first 8 characters of the session ID as a parameter. I have to dig through ~/.paseo/agents/<project>/*.json files to find the corresponding agent’s persistence.sessionId—annoying every time.
Solution: write a wrapper script that auto-maps tab names to sessionIds.
Pain Point 2: The Problem Recurs; We Need Preventive Maintenance
Rescue is “post-incident remediation”—you hit the error and then fix it. But as usage volume grows, sessions accumulate more screenshots, and it might break again the next day. Better to run automatically every day with safety guardrails that don’t interfere with active conversations.
We need four safety valves:
- Skip active sessions: If a session’s corresponding
claudeprocess is running right now, skip it—avoid write conflicts. - Skip recently modified: JSONL changed in the last N minutes? User might be chatting; skip.
- Image count threshold: Sessions with fewer than N images stay as-is—preserve recent visual context.
- Backup rotation: Each run creates another
.bakfile; over time they accumulate. Keep only the last K.
Tooling
I’ve assembled a set of scripts: they leverage the upstream ianbmacdonald/claude-session-rescue for JSONL image stripping, with a wrapper layer that’s Paseo-friendly and includes both manual rescue + scheduled versions + 4 safety valves. The code snippets below you can adapt directly.
scripts/paseo_rescue.sh — Manual Rescue, Paseo-Friendly
# List all named tabs and image counts
bash paseo_rescue.sh
# Preview changes
bash paseo_rescue.sh MyTabName --dry-run
# Actually rescue
bash paseo_rescue.sh MyTabName
# Batch rescue
bash paseo_rescue.sh --all
The script parses the title field and persistence.sessionId from ~/.paseo/agents/<project>/*.json and auto-maps them. Before running, just set the PASEO_AGENTS_DIR variable at the top of the script to your project slug.
scripts/paseo_rescue_auto.sh — Daily Scheduled Version + 4 Safety Valves
Core code snippet:
while IFS='|' read -r name sid cwd; do
jsonl="$CLAUDE_PROJECTS/${cwd_enc}/${sid}.jsonl"
# Valve 1: active claude process
if pgrep -f "claude.*${sid:0:8}" >/dev/null; then
log "[$name] skip: active process"
continue
fi
# Valve 2: modified in last 30 minutes
mtime=$(stat -f%m "$jsonl")
if [ $(($(date +%s) - mtime)) -lt 1800 ]; then
log "[$name] skip: recent write"
continue
fi
# Valve 3: image count threshold
img_count=$(grep -o '"type":"image"' "$jsonl" | wc -l)
if [ "$img_count" -lt 5 ]; then
log "[$name] skip: only $img_count images"
continue
fi
# Clean up and log
python3 "$RESCUE_SCRIPT" --session-id "${sid:0:8}"
# Valve 4: .bak rotation
ls -1t "${sid}.jsonl.bak"* | tail -n +4 | xargs rm -f
done < <(list_agents)
Pair this with a macOS LaunchAgent or Linux systemd timer (template examples are in the repo), and run it once daily at 18:30 local time. When cleanup happens, push a summary to Telegram; skip notifications for dry runs.
Monitoring Integration
My local monitoring system brings this scheduled task into a unified “backup jobs” dashboard:
- Task status (green/yellow/red)
- Last success time, next run time
- Number of sessions cleaned, space freed, images processed
- Failures auto-alert to Telegram
This uses my own monitoring framework (out of scope here to avoid coupling), but you can adapt your monitoring to the log format that paseo_rescue_auto.sh produces.
Practical Prevention Tips (Prevention > Rescue)
The best rescue is never needing one:
- Resize screenshots to ≤ 1920 px wide before pasting. On macOS, use
sips -Z 1920 file.png, or use third-party tools like Shottr / CleanShot that default to 1× output. - Moderate screenshot use in long sessions. If you can describe it in text, skip the image.
- For heavy users (pasting 10+ screenshots daily), set up a daily scheduled cleanup with
--min-images 5or so. - For DevOps / research work, have Claude summarize conclusions into text before cleanup—images are gone, but the text memory remains.
Side Note on API Rules
I haven’t seen this Anthropic multi-image rule documented prominently in official docs. I only reverse-engineered it after hitting the error. The “rule triggers, then retrospectively checks history” design is unfriendly to clients—users feel like “I only pasted a small image, how is it failing?” If Claude Code could show a helpful error like “Image N in message M exceeds limits,” UX would improve significantly.
But the rule is what it is. The best we can do is smooth out the rescue workflow. This tooling lets what was once a chaotic “scrambling every time” situation become “18:30 automatic cleanup, I usually don’t even notice.”
Links
- Upstream core: https://github.com/ianbmacdonald/claude-session-rescue (the JSONL image-stripping algorithm this post’s wrapper entirely depends on)
If this helps, share feedback on any issues encountered at X @panshi8848.
📷 Cover image · Flux Schnell AI · Panshi.io brand aesthetic