R&D Management 19 min read

Claude Code's Partial Load Bug: How Write Overwrites Unseen File Content

An analysis of a Claude Code bug where rule files exceeding context limits were only partially loaded via instruction injection, yet the Write tool's pre-check consulted a different registry — allowing full overwrites of unseen content until version 2.1.312 moved the integrity check to the write moment, requiring full reads or in-place edits.

Architecture Development Notes
Architecture Development Notes
Architecture Development Notes
Claude Code's Partial Load Bug: How Write Overwrites Unseen File Content

Prefix and One Full Write-back

Assume a team splits conventions into several files under .claude/rules/, one about 180,000 characters. The on-demand load limit is defined by session window: 40,000 characters for a 200K window, 200,000 characters for a 1M window on Claude Opus 4.6 and Sonnet 4.6, and 150,000 characters on newer models. In the same 1M window, one session type gets the whole file while another gets only the first 150,000 characters, leaving 30,000+ characters unseen.

The prefix the model sees ends with a sentence telling it to use the Read tool for the rest. It either doesn't read or reads only a segment. Then you say "organize this file." Write does a whole-file replacement — no append, no merge — and the written content lacks those 30,000+ characters.

Another constraint: the "use Read to read the remainder" suggestion may not be fulfillable in one go. Read refuses to read files over 256 KB in a single call; an 180,000-character Chinese file at ~3 bytes/char (UTF-8) is ~540 KB, so a supplementary read is itself a paginated operation.

This is exactly the scenario described in the 2.1.312 fix notes: after seeing only the beginning of an extremely long CLAUDE.md, rules file, or @-imported file in a subdirectory, Claude overwrote the entire file; now Write requires it to read the whole file or switch to in-place editing.

The interesting part isn't the bug itself but why it held. A truncated view and a whole-file write are each fine in isolation: truncation is a context-budget result, whole-file replacement is a legitimate tool capability. The failure lies in the middle — the two operations judged "how much of this file was seen" using different records.

Truncation Position Determined by Budget

First, the numbers that decide where truncation happens:

Read tool defaults to 25,000 tokens max per call; files over 256 KB are rejected outright, forcing the model to use offset and limit for pagination; since 2.1.296, allow_large can read the whole file at once, quota drawn from remaining session context.

Bash output uses a separate path: inline ~30,000 characters, beyond that it returns a disk file path plus a 2,000-character preview.

Instruction files follow a third path: on-demand loaded CLAUDE.md, rules files, and @path imports each have their own per-file character caps.

Three sets of numbers, three units, three different handling paths. This isn't surprising — the three content types have different context-entry cost structures: tool results are replayable (the model can re-read), while injected instructions happen automatically at session start or when a file is touched, and the model has no intrinsic motive to re-read unless told.

But all three produce the same outcome: the context contains the beginning of a file, not all of it. The only real difference is how that fact gets recorded.

Only One Path Turns "Not Fully Read" into State

The Read tool is stateful. When a file exceeds the default quota, it returns the first page with a PARTIAL view notice showing how much of the full file was received and how to continue with offset and limit. This notice isn't just human documentation — it's a queryable fact. Edit's read-before-edit check explicitly states: a read truncated by a PARTIAL view notice does not count.

Bash is weaker: after exceeding the limit the model gets a file path and a small preview; the rest it must fetch itself.

Instruction injection is weakest: the prefix is followed by "use the Read tool to read the remainder." That's a suggestion, not a queryable state.

Before 2.1.312 the chain broke like this:

On-demand loaded rules files only contributed a prefix; the session had no record saying the file wasn't fully seen.

Write's pre-check asked "has this file been read?" — the answer depended on that record.

Since 2.1.228, new models can overwrite a file never read in the current session, provided reading it wouldn't trigger a permission prompt and the Read tool is available — meaning "no record" was treated as "not read, okay to write."

And "not read" and "not fully read" landed on the same side of the check.

I pieced this mechanism from the document's wording; maintainers haven't spelled it out step by step. What's certain is the result: the 2.1.312 fix notes admit the file was overwritten whole after only its beginning was seen.

The gap between a one-sentence suggestion and a queryable state is exactly that overwrite.

Loosening One Gate Makes Another More Critical

The 2.1.228 change is reasonable: it aligns Write with Edit's rules, letting new models skip a round-trip for a read whose outcome is certain. The cost is that the gate shifts from "all models must read first" to "new models may overwrite under specific conditions."

The document simultaneously gives PARTIAL view a hard exemption: Jupyter notebooks, and files truncated by PARTIAL view, require a prior read from any model, otherwise the write returns an error. Together the logic is clear — round-trips can be saved, but the judgment basis cannot. The relaxation is on cost; the tightening is on the prerequisite. The problem is only where that prerequisite lives and who writes it.

2.1.309 on October 22 added the per-file cap to on-demand instruction files. Previously these files had no such limit, causing super-large nested CLAUDE.md files to trigger repeated session compressions — the changelog entry is blunt. Adding the cap is correct, but it simultaneously turns "only seeing the beginning" from an edge case into the norm: any rules file longer than the cap starts every session as a prefix plus a one-sentence hint.

Five days later, 2.1.312 patched the write-side check.

This chain only matters when the file is already long. The docs suggest keeping each CLAUDE.md under 200 lines; 200 lines of Chinese is under 10,000 characters, while the 200K window cap is 40,000 characters — about 4x the guideline. So files that reach truncation are usually far beyond the recommended length. This doesn't weaken the conclusion but changes the applicability: it's not an everyday pitfall, but a boundary hit when convention files grow unchecked. Script-generated rules or bulk imports from elsewhere cross that line more easily than hand-written ones.

Moreover, on-demand loading isn't one-off. The docs say the project-root CLAUDE.md is re-read after compression; subdirectory CLAUDE.md and rules with paths are reloaded on next file match. The same gap therefore recurs within a single session: every reload again yields only the prefix.

The order matters: first restrict the load range, then add the write-time integrity condition. Reversing it makes no sense — without truncation the check isn't needed, and doing the check before truncation would miss exactly what it's meant to catch.

Fix at Write End, Not Making the Prompt More Prominent

2.1.312 had two possible fixes: make the trailing hint in the injected prefix more prominent, or move the checkpoint to the write moment. It chose the latter.

The reason is clear. The prefix-end hint is a text segment weighed alongside thousands of other tokens, and in long sessions it gets compressed away; the same sentence at the moment of write — in the context "model is about to write this file" — carries completely different weight.

One distinction must be stated honestly. For PARTIAL view -truncated files the document states a hard condition: an unread whole-file write returns an error; but the 2.1.312 fix description says "Write now asks Claude to read fully or edit in place," using "asks." Whether it returns an error or a guiding message, I haven't constructed the scenario to verify — only that it's placed at the write moment.

Either way the design trade-off is visible: don't push responsibility onto Read. Read can't return content it doesn't have; truncation is a context-budget result, not a tool defect. The only decidable thing is which operations are allowed under the premise "only a part was seen." Whole-file replacement obviously belongs to the disallowed category. In-place editing does not, because old_string must match the current file content character-for-character; parts the model hasn't read won't be written back, and a failed match is rejected.

Put Into Your Own Toolset

If you're building an agent with write capability, this fix decomposes into three immediately checkable items.

First, every channel that feeds file content into context must register "how much of this file was seen" in the same place. In Claude Code that registry comes from Read tool results, so the instruction-injection prefix introduced in 2.1.309 started off the books. A common vulnerability in toolsets is multiple entry points for the same content: read tool, shell cat, retrieval results, template injection — each writes its own hint, each says "you can continue reading."

Second, whole-file overwrite tools must consult that registry before writing; missing record defaults to unread. Default-allow (no record → allow write) is the worst choice here, because a missing record almost always means "content came in through another path."

Third, if the cap varies by model or context window, it cannot be a deployment constant. The doc gives three numbers; the newer model gets the smaller one: an 180,000-character rules file is complete on Opus 4.6 but gets its tail cut on the newer model. Same commit, two people open sessions, they see different rules. To judge whether a file was fully loaded, you must compute against the session's actual window and model, not against the repo's expected style.

The pseudocode below isn't Claude Code's implementation; it just writes the fix's preconditions to make clear where the check sits:

# illustrative, not actual implementation
seen = {"kind": "partial", "shown": 150000, "total": 182400}

def write_whole_file(path, content):
    if exists(path) and seen_for(path).get("kind") != "full":
        return "read it all first, or edit in place"
    return replace(path, content)

The check is at the write entry, not at read time, not in the hint.

Still Unclear Areas

The document lists PARTIAL view as a trigger condition but doesn't say whether an intentional paginated read using offset and limit counts. Literally, paginated read is Read's normal mode and shouldn't trigger the notice; semantically, the model still only saw part of the file. This blank exists in your own toolset too: after a paginated read, a whole-file overwrite — should it be blocked or allowed? Depends on whether you treat "deliberate partial read" as a known state.

Another boundary is above the cap: files over 4 MiB are skipped entirely by Claude Code — not a single byte loaded, hence no prefix, no hint. This is more thorough than truncation — truncation at least leaves a detectable cut; skipping leaves nothing, and the model doesn't even know the file exists.

Both are inferences from the docs, not documented conclusions. The first decides whether the check leaks; the second decides whether anyone discovers the leak.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

file handlingbug analysiscontext windowClaude Codeagent toolingPARTIAL viewRead toolWrite tool
Architecture Development Notes
Written by

Architecture Development Notes

Focused on architecture design, technology trend analysis, and practical development experience sharing.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.