[BUG] Changed instruction files are re-appended in full with no size check, which can lock a live session

Status Open
Reported on v2.1.284
Maintainer reply None cached
Activity 0 comments · opened Sep 30, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When an @-imported instruction file changes on disk, the next re-read appends that file's entire
current content
to the conversation. There is no diff, no size cap, and no check against the
remaining context window. If the append does not fit, the session is stuck.

1. The whole file is re-sent, not a diff.
A 22-byte edit to a 150 KB imported file re-sent 150,337 characters.

2. Nothing checks whether it fits.
A session sitting at 74% of a 200k window got a 47-byte edit to a 259 KB import. The next turn failed.

3. The earlier copy is not removed.
The new content is introduced as "each replaces its earlier copy", but the earlier copy stays in the
history and is sent on every later request. Measured: 92,235 prompt tokens before the edit, 147,254
after — every later turn carries the extra 55,019.

4. The session cannot recover itself.
/compact on the stuck session reports it cannot reduce below the limit. The only recovery I found was
editing the session transcript file offline.

5. There is no way to opt out.
CLAUDE_CODE_DISABLE_CLAUDE_MDS drops all instruction files rather than just the reload. The
InstructionsLoaded hook fires on this exact path but is observe-only — a hook returning
decision:block, continue:false, permissionDecision:deny and exit 2 together changed nothing,
and its additionalContext never reached the context.

6. Editing the file is not the trigger.
A process attaching to the session is (startup or resume). So the common shape is: an imported file
changes while a session is idle, the session is resumed, and the first turn after the resume fails.
Compaction, settings sync and a few other cache-invalidation events trigger it too.

7. Where it is in the bundle (2.1.284).
Gfo selects changed files by whole-content inequality and returns them intact:

function Gfo(e,n){ if(!n) return e.length===0?void 0:{files:e.slice()};
  let r=e.filter(h=>{let S=n.get(h.path); return !S || S.content!==h.content || S.type!==h.type}),
      s=new Set(e.map(h=>h.path)), g=[...n.keys()].filter(h=>!s.has(h));
  if(r.length===0&&g.length===0) return;
  return {files:r, ...g.length>0&&{removed:g}, changed:!0} }

o5n then renders each file verbatim and z2e wraps them with "Instruction files were re-read ...;
these differ from their earlier copies". No token count or size comparison appears on this path.

Invalidation reasons passed to BT(session, reason): session_start, compaction, policy_refresh,
settings_sync, account_change, directory_added, hooks_invalidate, policy_verdict,
memory_paused, memory_resumed.

8. The same defect is open against another CLI agent.
openai/codex#43295 — reload on cold resume, whole bundle appended, earlier copy kept,
context_window_exceeded at roughly 65% of the window. Two independently built harnesses reached the
same design and the same faults, which suggests this is the easy shape to land on rather than a
one-product oversight.

What Should Happen?

Check the size against the remaining window before appending. That single check prevents the stuck
session regardless of which strategy is chosen below.

Ask, using the confirmation menu that rewind already uses. Esc Esc puts up a numbered menu with
"Confirm you want to restore...", a nevermind option, an onSummarize path, and a caveat line
("Rewinding does not affect files edited manually or via bash."). A reload is the same class of event
and could reuse it:

Reload

Confirm you want to reload CLAUDE.md

> CLAUDE.md [X bytes] has been changed [X bytes changed]

1. Append entire file to context
2. Summarize the changes only and append
3. Compact and reload
4. Reload in place
5. Never mind

> Reload in place will result in a cache miss and may also result in erratic model
  behavior depending on the nature of the changes

Option 2 already exists as a capability — the rewind menu has an onSummarize handler. Option 5 is
the case that is missing today: there is currently no way to decline.

Prefer a diff over the whole file. 47 changed bytes should not cost 259 KB.

Compact before appending, not after. The compaction path already drops changed instruction
attachments from the kept tail, so it produces the right result — but only if it runs before the
append. After the prompt is already oversized it cannot help.

If a copy is replaced, remove the old one. Or stop telling the model it was replaced.

Surface the reload. Even without a menu, a one-line notice ("3 instruction files changed on disk,
reloading N KB") would let someone act before the next turn instead of discovering it by failure.

Error Messages/Logs

Failing turn:

    Prompt is too long

Attempting to recover the same session:

    Compaction failed - conversation could not be reduced below the context limit

Session transcript rows around the failure (`~/.claude/projects/<slug>/<session>.jsonl`):

    instructions  reason=null           changed=null  nfiles=9  chars=282,544   <- initial load
    instructions  reason=session_start  changed=true  nfiles=1  chars=259,039   <- 47 bytes changed
    assistant     model=<synthetic>  isApiErrorMessage=true  "Prompt is too long"

The 259,039-character row is the full current content of the one file that changed. The 282,544-char
initial row is still present after it.

Steps to Reproduce

Self-contained, about two minutes. Sizing note below matters.

R=/tmp/cc-overflow-repro; rm -rf $R; mkdir -p $R; cd $R

python3 -c "
import random,string; random.seed(11)
L=['## Section %d\n\nFact %d: inventory record %d holds token %s and checksum %s.\n'
   %(i,i,i,''.join(random.choices(string.ascii_lowercase,k=28)),''.join(random.choices('0123456789abcdef',k=16)))
   for i in range(2150)]
open('BIG.md','w').write('# Large imported file\n\n'+''.join(L))"     # 258,993 bytes

printf '# Repro\n\n@BIG.md\n' > CLAUDE.md
SID=$(python3 -c 'import uuid;print(uuid.uuid4())')

# turn 1 -- healthy. In my run this sat at 147,722 / 200,000 tokens (74%).
claude -p "Reply with exactly: T1" --session-id "$SID" \
       --model claude-haiku-4-5-20251001 --permission-mode bypassPermissions

printf '\n## Appended section 9999 -- one new line only\n' >> BIG.md   # +47 bytes

# turn 2 -- resume
claude -p "Reply with exactly: T2" --resume "$SID" \
       --model claude-haiku-4-5-20251001 --permission-mode bypassPermissions

Expected: T2. Actual: Prompt is too long, and every later turn in that session fails the same way.

Then inspect:

jq -r 'select(.type=="attachment" and .attachment.type=="instructions")
  | "reason=\(.attachment.reason) changed=\(.attachment.changed) nfiles=\(.attachment.files|length) chars=\([.attachment.files[].content|length]|add)"' \
  ~/.claude/projects/-tmp-cc-overflow-repro/$SID.jsonl

Sizing: your baseline differs because your own ~/.claude/CLAUDE.md and rules/ load too. Size the
fixture so the initial load fits the window and one extra copy of it does not. With an empty
CLAUDE.md my fixed overhead measured 38,365 tokens.

Smaller variant that shows the full-copy behaviour without needing an overflow: use a 150 KB BIG.md,
append one line, resume, and observe changed=true chars=150,337 for a 22-byte edit.

Claude Model

Other

Is this a regression?

Yes, this worked in a previous version

Last Working Version

Unknown by version number but live edits to claude.md did not auto-reload before fable launch

Claude Code Version

2.1.285. Behaviour observed on 2.1.278, 2.1.284 and 2.1.285.

Platform

AWS Bedrock

Operating System

Ubuntu/Debian Linux

Terminal/Shell

Other

Additional Information

Environment details the form dropdowns cannot hold. Model: reproduced on Claude Haiku 4.5, first observed on Fable 5.1 (1M context). The code path has no model branch, and a second session that was live across the same file edits but never resumed was unaffected, so the trigger is the resume rather than the model. Regression: yes; see Last Working Version for why I cannot name the version. Platform/OS/terminal: Linux x86_64, Ubuntu 24.04.5 LTS, kernel 7.0.0-31-generic, bash 5.2.21.

Why this is easy to hit. Anyone running more than one session against a shared instruction tree
is exposed: one session edits an imported file, another session resumes hours later and dies on its
first turn. The failure surfaces far from its cause, and nothing announces the reload.

Cost, separate from the overflow. A reload that does fit still appends a second copy that is sent
on every later request until a compaction. On the 150 KB fixture a 22-byte edit added 55,019 tokens per
turn. Someone might reasonably want the mid-session reload off for that reason alone, which is what
option 5 in the menu above is for.

The codebase already caps instruction-shaped content elsewhere.
CLAUDE_CODE_MAX_MCP_DESCRIPTION_LENGTH bounds MCP tool descriptions, and the Code Review path reports
when REVIEW.md instructions were cut for exceeding a size limit. This path has no equivalent.

CLAUDE_CODE_RESUME_TOLERATES_CONTEXT_APPENDS is present in the env table and the name looks
related to this, but I could not determine what it does. If it is the intended control, it is
undocumented.

A better error message would have saved a lot of time. The failure said only Prompt is too long.
Naming the cause — that instruction files were re-read and how large the append was — would point
straight at it.

Recovery, for anyone else who hits this. Fold each changed file into the session-start
instructions row, delete the changed=true rows and the synthetic error row, then rewrite the
transcript. Two details decide whether it works: fold the current on-disk content rather than the
content from the injected row, because the harness compares disk against the transcript copy on the
next attach and the files usually drift further in the meantime; and normalise with .trim(), which is
what the loader stores. I have a script that does this with a dry-run mode and can share it if useful.

View original on GitHub ↗