[BUG] Read tool double-persists binary files in session JSONL (toolUseResult.file.base64 + document content block)
Summary
The Read tool persists every binary file (PDF, image) twice in the session JSONL: once as toolUseResult.file.base64 and once as a document/image content block in the next user-role message. The two copies are byte-identical. A single 1.6 MB PDF read costs ~4.4 MB of session-file disk and replays in every subsequent assistant turn's request payload.
Environment
- Claude Code version: 2.1.149
- Platform: Windows 11 (win32) — also seen reported in other-OS contexts via #16251
- Python: 3.13.13
- Project: contains PDF reads (vet records, ~1.6 MB each)
Steps to Reproduce
- In any project, use the Read tool to load a 1–2 MB PDF:
Read("/path/to/file.pdf"). - Locate the session JSONL under
~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl. - Inspect the line containing that tool result and the line immediately after.
import json
with open("<sessionId>.jsonl", "rb") as f:
lines = f.readlines()
# Line N: the tool result for the Read call
d_n = json.loads(lines[N-1])
b64_a = d_n["toolUseResult"]["file"]["base64"]
# Line N+1: the next user message Claude Code emits, containing the document block
d_np1 = json.loads(lines[N])
b64_b = d_np1["message"]["content"][0]["source"]["data"]
print(b64_a == b64_b) # True
print(len(b64_a), len(b64_b)) # identical, ~2.18 MB each for a 1.6 MB PDF
Actual Behavior
Measured across one author's project (help-cats) sessions:
| session | size | binary reads | shadow copy (toolUseResult.file.base64 only) |
|----------------------------------------------|-------|---------------------|-----------------------------------------------|
| c5704fcc-…jsonl | 29.3 MB | 5 PDFs + 23 images | 9.6 MB (33%) |
| 9e7ee380-…jsonl | 6.3 MB | 13 images | 1.7 MB (26%) |
| c74123b7-…jsonl | 3.7 MB | 2 PDFs | 0.1 MB (3%) |
The document/image content-block copies add the same amount again (verified byte-identical for the c5704fcc case: lines 454/455 hold the same 2.18 MB base64 for tora tran.pdf). So for that session ~20 MB of 30 MB is pure duplication of binary payloads.
Expected Behavior
The session JSONL stores each binary file's bytes at most once per tool call. Possible designs:
- Drop
toolUseResult.file.base64entirely if the next emitted user message already contains the same content block (the API-bound copy is the load-bearing one). - Store the binary out-of-line in a sidecar file under the project directory, and reference it by relative path in both records.
- Strip
toolUseResult.file.base64on session-file write but keep it in memory for the in-process inspector / TUI.
Impact
- Disk: sessions inflate by ~2× the size of every binary file read. With 1–2 MB PDFs and multiple sessions, projects accumulate hundreds of MB quickly.
- Token cost on resume / continue: the full duplicated payload is replayed to the model on every subsequent turn (the content-block copy is in the conversation history), inflating prompt size for the rest of the session.
- UX: reading past sessions in
/resumeor scrolling the JSONL shows the same long opaque base64 string twice for every binary read — the user-visible "repeating texts." - Likely upstream cause of #16251 (
/resumecrashing on large tool results, closed): the duplication makes individual JSONL lines exceed parser limits roughly twice as fast as a single-copy design would.
Workaround
Pre-extract PDF text once with PyMuPDF and have Claude Read the .txt sidecar instead of the .pdf. Drops per-read cost from ~4.4 MB to a few KB. Doesn't help users who genuinely need the visual layout (forms, scans), but works for any text-bearing record.
Related
- #16251 (closed) —
/resumecommand crashing on large tool results.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗