[BUG] --resume re-sends the whole CLAUDE.md when only its path changed, not its content
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet. The closest I found is #85595, which is about which memory directory a subdirectory session resolves — a different defect from re-sending content that was already resolved. #38029 and #24147 are usage complaints without this mechanism.
- [x] This is a single bug report.
- [ ] I am one patch behind: this was measured on
2.1.276, and2.1.277is published as I write. The behaviour lives in the resume path and reproduced on demand across every arm below, so I do not believe a patch version changes it — but I would rather say so than tick the box.
What's Wrong?
When a session is resumed with claude --resume from a different working directory, the CLI re-sends the entire CLAUDE.md as a fresh instructions attachment, marked changed: true, reason: "session_start" — even when the file it sends is byte-identical to the one the session already holds.
The re-resolution looks correct to me and I am not asking for it to change: the docs say discovery walks up from the working directory, so a different directory genuinely resolves a different set. The waste is the re-sending of content the session already has.
It is not simply "the set changed, so re-send the set". The CLI already diffs the memory-file set per path. I saw this directly: resuming from a parent directory, where the child's own CLAUDE.md drops out of the resolved set, produced
{"type":"instructions","files":[],"removed":["…/child/CLAUDE.md"],"changed":true,"reason":"session_start"}
The parent's file, whose path had not changed, was not re-sent. So the comparison is per path. What it does not do is notice that a path newly entering the set carries text the session has already been given — and in that case the whole file is written again.
What Should Happen?
Compare the content of a memory file entering the resolved set against what the session already holds, and skip the re-send when it matches — by hash, or by whatever normalised form already makes a comment-only edit not count as a change. (I observed that an edit confined to HTML comments did not trigger a re-send, while a one-line content edit to the same file did, so some normalisation already exists.)
Two smaller things that would also help, if the above is hard:
- A documented way to pin or carry a memory-file set across a resume that changes directory. I could not find one — no flag, setting, environment variable or hook.
- A note in the resume documentation that memory files are re-resolved from the new working directory. The sessions page lists
--mcp-config,--settings,--plugin-dirand--add-diras things to pass again, and says nothing about memory files, so the cost is invisible to anyone reading the docs.
Steps to Reproduce
No repository needed. Two directories and three commands.
- Make two directories and put the same
CLAUDE.mdin both. Confirm withcmp— if you skip this you may end up measuring the content trigger instead without noticing. - Seed a session in the first directory and keep its
session_id:
````
claude --print --output-format json --model haiku "say ok"
- Resume that session from the second directory:
````
claude --print --output-format json --model haiku --resume <id> "say ok"
- Open
~/.claude/projects/<slug of the FIRST directory>/<session id>.jsonl— the transcript stays under the directory the session was created in. The resume carries aninstructionsattachment withchanged: trueand the whole file, although nothing about the file changed.message.usage.cache_creation_input_tokensshows the write.
A control that separates the two directions cleanly: do the same with nothing changed and no directory change. That wrote 119 tokens, against 17,084 for a one-line content edit to the same file.
My measured arms, for reference:
| arm | what changed between seed and resume | whole file re-sent? | tokens written on the resume |
|---|---|---|---:|
| A | nothing | no | 119 |
| B | one line of content added to a 72,506-character file | yes | 17,084 |
| C | two HTML-comment lines added | no | 178, then 123 |
| D | one line of content added (79,660-char file) | yes | 21,577 |
| E | nothing at all — same file, resumed from a DIFFERENT directory | yes | 23,253 |
Arm E is the bug. Both copies were 79,595 characters, confirmed with cmp.
Why this costs real money
Our deferred-findings automation works one item in two turns of one session: a research turn in a throwaway git worktree, then a --resume for the implementation turn in the enclosing checkout. The two directories held identical copies of the same CLAUDE.md, so every handover re-sent the whole file. Ours is about 21,400 tokens.
The write is the smaller half. The extra copy then sits in the conversation and is read again on every later request of that session. Over 14 days on one machine:
| what | figure |
|---|---:|
| handovers | 58 |
| of those, path-only (identical length, different directory) | 45 |
| requests after the re-injection, per handover | median 125, mean 145, max 275 |
| extra tokens re-read because of it | 175.7 M |
| all cache-read tokens in those sessions after the re-injection | 2,300.3 M |
About 7.6% of everything those sessions read. On claude-opus-5 that is roughly $88 of re-reading plus $8 of writing per fortnight, on one machine.
Please weigh that table knowing two things. The scan classified a re-injection as path-only by comparing content length rather than bytes, so some of the 45 could be same-length edits; the truly byte-identical case is proven by arm E instead. And the handover counts, the split, the request counts and the model come straight out of the transcripts, but the re-read total and the money are derived — measured per-request delta times measured request counts. Nobody has watched one request's read figure drop by 21,400 after removing the re-injection, so treat the money as the right order of magnitude rather than a measurement.
Our workaround, for context
We now delete the memory file from the throwaway worktree, so both turns resolve the enclosing checkout's copy at one path and nothing new enters the set. That works because the discovery walk reaches parent directories — including across a nested git checkout — but it only works for people whose second directory happens to sit inside the first. Anyone whose two directories are siblings still pays.
Claude Model
Haiku for the measurements, deliberately — the behaviour is in the CLI, not the model. The production sessions that pay for it run Opus.
Is this a regression?
I don't know.
Claude Code Version
2.1.276 (Claude Code)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
VS Code integrated terminal, Git Bash
Additional Information
Measurements were taken at the 1-hour prompt-cache lifetime. Set CS_BEEP_SUPPRESS=1 before the --print calls or each one fires the notification chime.