[BUG] The external-file-change system note asserts an unverifiable cause ("by the user or by a linter") and the user's awareness — the model relays it as fact

Status Open
Reported on v2.1.193
Maintainer reply None cached
Activity 4 comments · opened Jun 26, 2026

Preflight

  • [x] I have searched existing issues and this specific angle isn't reported
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code (2.1.193)

Summary

When a tracked file changes between a Read and the agent's next read/edit — from a cause other than the agent's own tool call — Claude Code injects a <system-reminder> describing the change. The note asserts a specific cause ("either by the user or by a linter") and that the user is "already aware." Both are guesses the harness can't verify: the change can equally come from a concurrent background task the same session started (a backgrounded Bash command or Agent/Task), an external tool, or any other process. The model has no reason to distrust a system note, so it treats the guess as fact — relaying a false provenance to the user ("you edited X", "a linter changed Y") and staying silent about a change the user never made.

The injected note (verbatim)

Note: <file> was modified, either by the user or by a linter. This change was intentional, so make sure to take it into account as you proceed (ie. don't revert it unless the user asks you to). Don't tell the user this, since they are already aware. Here are the relevant changes (shown with line numbers):
<diff>

It bundles three assertions, all of which only hold if the user (or a linter) actually made the change:

  1. Cause — "either by the user or by a linter." Omits the common case of a concurrent process, including background work the session itself spawned.
  2. Intent — "This change was intentional."
  3. Awareness + silence — "Don't tell the user this, since they are already aware."

This is about the note's wording over-asserting, not the change-detection mechanism (cf. #25623): even when detection correctly reports that the file changed, the asserted cause and awareness are unfounded.

Why this matters

  • Confabulated provenance. The model relays the guess as known truth — "you edited this file", "a linter reformatted it" — when neither happened. This manufactures false certainty: the model ends up more wrong than with no note, because it now has a confident-sounding but baseless source. Observed in a session where a backgrounded Agent wrote to a file the main session had read — the note attributed the change to "the user or a linter," and the model then told the user it was their edit. Neither the user nor a linter had touched the file.
  • Suppressed surprise. "Don't tell the user this, since they are already aware" is correct only when the user made the change. If a concurrent task or external process changed the file unexpectedly, the user is not aware — and the note instructs the model to stay silent about exactly the kind of surprising change it should surface.

Suggested fix

The note should not assert a cause (or the user's awareness) it can't verify. Either:

  1. Make the wording cause-neutral — e.g. "<file> was modified by a process outside this tool call (an editor or formatter, a hook, the user, or a concurrent task)." State what is known — the file changed, here is the diff — and stop. Drop the "intentional" / "already aware" / "don't tell the user" clauses, which only hold for the user-initiated case.
  2. Condition the strong wording on actual knowledge — assert "by the user" / "already aware" only when the change was genuinely user-initiated (e.g. a tracked external editor), and fall back to neutral wording otherwise.

Option 1 is simplest and removes the false-certainty failure mode entirely.

Related issues

  • #63854 — redact secrets in the file-modification system-reminder content. Same note, different concern.
  • #62336 — a concurrent subsystem writing mid-turn invalidates Edit read snapshots. Same trigger condition (concurrent write), different surface.
  • #25623 — mtime-based change detection gives false positives (IDE autosave). The detection layer that fires this note.
  • #59994 / #9769 — granular control over <system-reminder> injections. A related knob.
  • #30735, #50331 — same class: injected system text states something that doesn't match reality, which the model then acts on.

Environment

  • Claude Code: 2.1.193
  • Model: claude-opus-4-8
  • Platform: Anthropic API (Claude Code CLI)
  • OS: macOS (Darwin 25.4.0)

View original on GitHub ↗

4 Comments

github-actions[bot] · 2 months ago

Found 1 possible duplicate issue:

  1. https://github.com/anthropics/claude-code/issues/62336

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

ark2042 · 2 months ago

Related angle on the same <file> was modified mechanism: cross-session context bleed.

In multi-session orchestrators (e.g. a Telegram bot mapping each chat topic to its own Claude Code session, sharing one machine), readFileState tracks every file a session has read/written, keyed by absolute path and not bounded to the session's cwd. When another session edits such a file, the harness injects the <file> was modified reminder into the first session on its next turn.

Concretely this caused a real incident for us: a session focused on topic A (cwd = home, a meta/diagnostic channel) suddenly received a was modified reminder + diff for projectB/decisions.md — edited by a completely different session. The model, trusting the system note, bound the user's next ambiguous message to project B and drifted off-topic.

So beyond the provenance-wording problem this issue raises (which we hit too — the note's "by the user or by a linter" is wrong, it was another session), there's an isolation problem: the reminder fires across session boundaries with no cwd scoping and no client-side hook to filter it.

Two things would each independently fix our case:

  1. Scope these reminders to the session's cwd (ignore tracked files outside it), or expose a setting to do so;
  2. a hook to observe/suppress them client-side (requested separately in #57519, also stale).

Flagging here since this is the only open issue still tracking the was modified reminder. Happy to provide a minimal repro.

boaza · 20 days ago

Corroborating this with a fully root-caused incident, plus evidence that this wording is generating a large cluster of false security reports.

The note is stock, and I can prove where it comes from

Both strings are compiled into the shipped binaries — verified in the exact one my session ran (anthropic.claude-code-2.1.223-win32-x64/resources/native-binary/claude.exe, 2 matches each), and also present in the npm bin/claude.exe:

Lfc = {
  directory: (e) => ...,
  edited_text_file: (e) => Ep([Pn({content: e.snippet === ""
    ? `Note: ${e.filename} was modified, either by the user or by a linter. This change was intentional, so make sure to take it into account as you proceed (ie. don't revert it unless the user asks you to). Don't tell the user this, since they are already aware. The diff was omitted because other modified files in this turn already exceeded the snippet budget; use the Read tool if you need the current content.`
    : `Note: ${e.filename} was modified, either by the user or by a linter. This cha...

Trigger, per the binary's own schema description: "Model-facing note listing readFileState entries whose mtime bumped during this command (set when WRITE_COMMAND_MARKERS matches)."

Three real causes, none of them "the user or a linter"

Same session (2.1.223, VSCode extension, Windows 11). Three of these notes fired; the model distrusted the "don't tell the user" clause each time and disclosed anyway. Forensics afterwards found a distinct real cause for each:

1 — A concurrent Claude Code session stashed the files. A second session sharing the same working directory ran, from its own transcript:

git stash push -m "WIP: ... paused for map-extraction pickup" -- extract/run.py main.py

A pathspec-limited stash of exactly the two files the first session had just edited — a clean revert to HEAD, ~4 minutes after the edits. refs/stash@{...} confirms it, and the stash diff still contains the vanished work. The first session was told the change was intentional and that its user was aware. The user was not aware, and it was not the user.

2 — The session's own foreground subagent. An Agent dispatch wrote ~300 lines into a file at 19:02:27; the parent got [Request interrupted by user for tool use] 3h13m later and concluded the agent had never run (that half is #84621). The edited_text_file note then presented the subagent's output as an accepted external edit.

3 — Genuine multi-day session. The date_change sibling reminder (The date has changed... DO NOT mention this to the user explicitly because they are already aware.) fired correctly after a session sat open Aug 6 → Aug 10. Correct behavior, but the identical "already aware / do not mention" phrasing made it read as part of the same anomaly.

Cases 1 and 2 are exactly the "concurrent process" gap the issue body predicted — and case 1 involved silent loss of uncommitted work while the harness asserted the change was intentional and should not be reverted or mentioned.

The wording is producing a false-positive security cluster

Searching this repo for the phrase returns ~10 open issues from different users, nearly all framing it as an attack — "spoofed", "fabricated", "injected", several labeled area:security: #74636, #85126, #77324, #84891, #81503, #83970, #81127, #82814, #84484, #82767, #74651.

I believe most of these are this same benign mechanism plus an unverifiable causal claim. Two properties make it read as an injection to a security-aware model:

  1. The asserted cause is wrong often enough to be checkable — and when the model checks, it finds no user edit and no linter, which looks like a lie rather than a bad guess.
  2. "Don't tell the user this" is behaviorally indistinguishable from a prompt-injection payload. A model trained to refuse concealment instructions from untrusted content will correctly refuse it here too — and then reasonably escalate to the user as a suspected compromise.

Worth noting these reports are the good outcome. The failure mode is a model that complies: silently accepting a destructive concurrent-session change because the harness told it the change was intentional and the user already knew.

Suggested wording

Drop the cause, the intent, and the concealment instruction; keep the fact and the guidance:

Note: <file> changed on disk since you last read it. This was not your own edit.
The cause is unknown — it may be the user, a linter, a concurrent Claude Code
session, or a subagent you spawned. The current content is below; work from it and
do not revert it unless asked. If the change affects what you were doing, say so.

That is accurate in every case, keeps the anti-revert behavior, and removes the two properties that make the current text read as an attack. If a suppression hint is genuinely wanted for the common linter case, gating it on a verified cause (the harness knows when it ran a formatter) would keep the claim truthful.

Happy to supply the raw evidence — transcripts, stash refs, ~/.claude/file-history/ snapshots, process list — if useful for triage.

Environment: Claude Code 2.1.223 (VSCode extension), Windows 11 Pro 26200, two concurrent sessions sharing one git checkout.

milobird · 6 days ago

Update from testing on 2.1.241: the system-reminder path appears fixed — the injected note is now cause-neutral. Observed wording:

Note: <file> changed on disk since you last read it. That's usually deliberate, so take it as the current state rather than reverting it; if the change looks wrong, say so rather than undoing it yourself — otherwise no need to call it out. Here are the relevant changes (shown with line numbers): …

That resolves the reported failure mode for the reminder. However, the same over-assertion survives in the Edit tool's staleness rejection, which still reads (verbatim, 2.1.241):

File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.

This fires at exactly the decision point where misattribution matters most: the model has just tried to write, is told a linter or the user is responsible, and may conclude the change is cosmetic and re-attempt its original edit. In our multi-agent setup the actual author was a concurrent agent appending to a shared file — neither the user nor a linter. The same cause-neutral wording adopted for the reminder would fit here.

Two adjacent observations from the same tests, in case they're useful: the staleness handling is inconsistent across contexts (same shape of external mid-session write produced a hard Edit rejection in one session, a warn-but-apply note — "the edit applied cleanly, but the file contains other changes not in your context" — in another, and no signal at all in a third), and files read only via Bash (cat) are never tracked, so no notice ever fires for them. Happy to split those into separate issues if that's preferred.