claude-code - 💡(How to fix) Fix `@`-mention synthetic Read injection doesn't satisfy Edit/Write 'must Read first' guard, forces redundant Read [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#58574Fetched 2026-05-14 03:44:46
View on GitHub
Comments
0
Participants
1
Timeline
3
Reactions
0
Author
Participants
Timeline (top)
labeled ×3

When a file enters context via an @filepath mention in a user message, the harness injects a synthetic <system-reminder> that mimics a Read tool call + result (so the model has the file content without spending a real turn). However, this synthetic injection is not registered with the Read-tracker that Edit/Write consult before allowing modifications. The result: the model has the full file content, but the first Edit attempt is rejected with File has not been read yet. Read it first before writing to it., forcing a real Read call that returns identical bytes already in context.

Root Cause

The cost is not just "one extra tool call" — the two copies of the file's content are not deduplicated:

  • The synthetic <system-reminder> injection is one text block in the message history.
  • The real Read tool result is a separate text block in the message history.
  • Both blocks are replayed to the model on every subsequent turn for the rest of the session (until compaction).
  • Prompt caching reduces $ and latency on the cached prefix, but cached tokens still count against the context window — so caching does not mitigate the context-pressure cost.

Per @-mention-then-edit flow, the costs are therefore:

  • Context-window pressure: the file's full content occupies the window twice for the remainder of the session. For large files (long source files, generated docs, transcripts pulled in via @) this is the dominant cost — it shortens how much real work fits before compaction.
  • Wasted tokens on every subsequent turn (both copies billed against the cache-miss and cache-hit lines).
  • Wasted latency: one extra tool round-trip before any useful work begins.
  • Wasted permission prompts in stricter permission modes — the redundant Read may itself need approval.
  • Model confusion: the synthetic injection is presented in the exact shape of a real Read tool result, which makes the subsequent rejection feel like an inconsistency in the harness rather than a deliberate guard. A model that trusts the injection's framing will issue Edit first, hit the rejection, and have to recover.

Code Example

Called the Read tool with the following input: {"file_path":"…"}

---

File has not been read yet. Read it first before writing to it.

---

@README.md please add a heading at the top called "Notes"

---

Edit(README.md,)

---

File has not been read yet. Read it first before writing to it.
RAW_BUFFERClick to expand / collapse

Summary

When a file enters context via an @filepath mention in a user message, the harness injects a synthetic <system-reminder> that mimics a Read tool call + result (so the model has the file content without spending a real turn). However, this synthetic injection is not registered with the Read-tracker that Edit/Write consult before allowing modifications. The result: the model has the full file content, but the first Edit attempt is rejected with File has not been read yet. Read it first before writing to it., forcing a real Read call that returns identical bytes already in context.

Environment

  • Claude Code (Windows desktop)
  • Model: claude-opus-4-7
  • OS: Windows 11
  • Trigger: @-mention of a file in a user message

Steps to Reproduce

  1. Start a fresh Claude Code session.
  2. Send a message containing @path/to/some/existing/file.md (no prior Read of that file in this session).
  3. The harness injects a <system-reminder> of the form:
    Called the Read tool with the following input: {"file_path":"…"}
    plus a second reminder with the file's numbered contents (the standard Read result format).
  4. Ask the assistant to make a small edit to that file.
  5. Assistant calls Edit on the file.

Observed Behavior

Edit fails:

File has not been read yet. Read it first before writing to it.

The assistant then has to call Read on the exact same path. The Read returns byte-identical content to what was already in the synthetic injection. After that, Edit succeeds.

Net effect per @-mention-then-edit flow:

  • One redundant Read tool call.
  • The file's contents occupy context twice (once from the synthetic injection, once from the real Read).
  • One extra round-trip of latency.
  • Confusing for the model: the system-reminder is formatted as if a Read occurred, so the model reasonably assumes Edit will work.

Expected Behavior

The synthetic Read injection produced by @-mention attachment should register the file with the same Read-tracker that Edit/Write check. The Edit guard should treat the file as already read (with the same staleness/mtime semantics as a real Read), so no redundant Read is necessary.

Why this matters

The cost is not just "one extra tool call" — the two copies of the file's content are not deduplicated:

  • The synthetic <system-reminder> injection is one text block in the message history.
  • The real Read tool result is a separate text block in the message history.
  • Both blocks are replayed to the model on every subsequent turn for the rest of the session (until compaction).
  • Prompt caching reduces $ and latency on the cached prefix, but cached tokens still count against the context window — so caching does not mitigate the context-pressure cost.

Per @-mention-then-edit flow, the costs are therefore:

  • Context-window pressure: the file's full content occupies the window twice for the remainder of the session. For large files (long source files, generated docs, transcripts pulled in via @) this is the dominant cost — it shortens how much real work fits before compaction.
  • Wasted tokens on every subsequent turn (both copies billed against the cache-miss and cache-hit lines).
  • Wasted latency: one extra tool round-trip before any useful work begins.
  • Wasted permission prompts in stricter permission modes — the redundant Read may itself need approval.
  • Model confusion: the synthetic injection is presented in the exact shape of a real Read tool result, which makes the subsequent rejection feel like an inconsistency in the harness rather than a deliberate guard. A model that trusts the injection's framing will issue Edit first, hit the rejection, and have to recover.

Related Issues (same root cause — Read-tracker keyed too narrowly)

The Read-tracker only counts literal Read tool invocations. Several other paths put content in context but bypass the tracker:

  • #47904 — ExitPlanMode resets the Read-tracker for files created/read during plan mode
  • #53525 — Edit rejects "must be read first" after Bash/Write wrote the file in the same session
  • #21291 (closed) — Read state lost after user-message interruption
  • #34026 (closed) — Read state lost in long conversations
  • #17895, #18158, #16182 — Various Write/Edit "must read first" rejections after Read

This issue is another instance of the same family: the tracker should be unified across every path that legitimately puts a file's content into the model's context.

Suggested Fix

Whichever code path constructs the @-mention synthetic Read system-reminder should also call the same internal "mark file as read at mtime T" routine that the real Read tool calls. After that, Edit/Write guards will accept the file without a redundant Read.

Minimal Repro Transcript (sanitized)

User message:

@README.md please add a heading at the top called "Notes"

Assistant tool call:

Edit(README.md, …)

Tool result:

File has not been read yet. Read it first before writing to it.

Assistant has to then call Read(README.md), which returns the bytes already present in the prior <system-reminder> injection, before retrying Edit.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING