# Bug Report: Claude Code CLI crashes when reading large image files ## Summary The `Read` tool in Claude Code CLI fails when reading images larger than ~25K base64 tokens (~150KB file size). Small images work fine. The root cause is in the `DP1` image compression pipeline — when a large image goes through compression, the resulting API content block ends up with `source: {type: "base64"}` but **missing both `data` and `media_type` fields**. This causes an unrecoverable API 400 error. ## Environment - **Claude Code CLI:** `@anthropic-ai/claude-code@2.1.70` - **OS:** Windows 10 Pro for Workstations 10.0.19045 - **Node.js:** v24.13.1 - **sharp:** 0.34.5 (manually installed, works correctly) ## Root Cause Analysis ### The Size Threshold Images are read by `Nv8()` which calls `q01()` to create the result. After `q01()`, a size check runs: ```javascript if (Math.ceil($.file.base64.length * 0.125) > q) // q = Tv8() = 25000 tokens ``` - **Small images** (< ~150KB file / < 25K tokens base64): Skip `DP1`, return directly from `q01()` → **WORKS** - **Large images** (> ~150KB file / > 25K tokens base64): Enter `DP1` compression path → **CRASHES** ### What happens in the DP1 path When the image exceeds the token limit, `DP1()` is called to compress it. `DP1` uses sharp to resize/recompress and returns `{base64, mediaType, originalSize}`. The code then returns: ```javascript return {type: "image", file: {base64: H.base64, type: H.mediaType, originalSize: z}} ``` In isolation, this looks correct. `H.mediaType` is `"image/jpeg"` (from `vp6()` inside `DP1`). ### Where it actually breaks The tool result mapper converts this to an API content block: ```javascript case "image": return { tool_use_id: q, type: "tool_result", content: [{ type: "image", source: {type: "base64", data: A.file.base64, media_type: A.file.type} }] }; ``` **However**, between the mapper output and the actual API request, the image content block gets **stripped**. The API receives: ```json {"type": "image", "source": {"type": "base64"}} ``` Both `data` and `media_type` are absent. `JSON.stringify` silently drops `undefined` properties, so if both become `undefined` at any point, the serialized JSON omits them entirely. ### Evidence from transcript analysis The session transcript (`.jsonl` output) captured the exact message content sent to the API: ```json { "type": "user", "content": [{ "tool_use_id": "toolu_01NmuSjPErhBfbtoV8RBrJip", "type": "tool_result", "content": [{"type": "image", "source": {"type": "base64"}}] }] } ``` This confirms `data` and `media_type` are both missing at the API call level. ### The actual root cause (suspected) The image data stripping likely occurs in the **message normalization/storage layer** between the tool result mapper and the API call. When conversation messages are stored in memory (the internal `D` array or conversation state), large base64 image data may be: 1. Stripped for memory efficiency 2. Moved to a separate image attachment store (referenced by `imagePasteIds`) 3. Lost during `structuredClone` or message serialization The reconstruction step that should restore the image data before the API call **fails for tool_result image blocks**, possibly because it only handles top-level image blocks (from user pastes) but not images nested inside `tool_result.content[]`. ## Test Results | File | Size | Base64 tokens | DP1 path | Result | |------|------|---------------|----------|--------| | photo.jpg | 25KB | ~4,250 | No | **Works** | | test_tiny.png | 98B | ~16 | No | **Works** | | test_medium.png | 751KB | ~125,000 | Yes | **Crashes** | | screenshot_gui.png | 387KB | ~64,500 | Yes | **Crashes** | ## Severity: Critical - **Session-killing:** corrupted message poisons the entire conversation context - **No recovery:** every subsequent API call fails with 400 - **Affects subagents too:** Agent tool crashes, but main session survives - **Size-dependent:** only images > ~150KB trigger the bug ## Patches Applied ### Patch 1: Nv8 try/catch wrapper (`PATCHED_NV8_SAFE_IMAGE_READ`) Wraps the entire `Nv8` function in try/catch. On failure, returns a text error message instead of corrupted binary. Also adds `||"image/png"` fallback on `H.mediaType` in the DP1 path. ### Patch 2: Image mapper media_type fallback (`PATCHED_IMAGE_MEDIA_TYPE`) Adds `||"image/png"` fallback to `media_type` in the tool result mapper. Prevents `undefined` from being serialized as absent field. ### Effectiveness - Patches only work after restarting Claude Code (cli.js is loaded once at startup) - Patches fix the `media_type` issue but may NOT fix the missing `data` issue - The underlying cause (image data being stripped from stored messages) needs to be fixed upstream ## Patcher Tool ```bash node tools/patch_claude_code.js # Apply all patches node tools/patch_claude_code.js --check # Check status node tools/patch_claude_code.js --revert # Revert to backup ``` After updating Claude Code (`npm update -g @anthropic-ai/claude-code`), re-run the patcher. ## Workarounds 1. **Use subagent for ALL image reading** — crashes in isolation, main session survives 2. **Resize large images before reading** — keep under ~150KB 3. **Read images only via Bash tool** — `file screenshot.png` for metadata, avoid actual content ## Files Referenced - **Patcher:** `tools/patch_claude_code.js` - **CLI entry:** `node_modules/@anthropic-ai/claude-code/cli.js` (minified, ~13K lines) - **Key functions:** `Nv8` (image reader), `DP1` (compressor), `q01` (result builder), `ig` (sharp wrapper), `mapToolResultToToolResultBlockParam` (API mapper) ## Report Info - **Date:** 2026-03-06 - **Version:** Claude Code 2.1.70 - **Reproducible:** 100% on Windows with any image > ~150KB