Edit tool: false "String not found in file" for multi-line old_string byte-identical to file content (file-state desync after successful edits)

Status Open
Reported on v2.1.183
Maintainer reply None cached
Activity 0 comments · opened Jul 16, 2026

Summary

The Edit tool intermittently returns "String not found in file. Failed to apply edit." for a multi-line old_string that is byte-identical to the file content on disk at call time. Verified two occurrences on the same day, on two different markdown files, with a distinctive pattern. Post-hoc transcript analysis proves the tool refused strings that were present in its own recorded file snapshots.

Environment

  • Claude Code 2.1.183, Linux (kernel 6.17, Ubuntu), VS Code extension host
  • Model: claude-fable-5
  • Permission mode: acceptEdits; the target path is in an allowed additional working directory

Repro pattern (occurrence 1 — 8 Edit calls to one ~6KB markdown file)

  • Outcomes alternated perfectly: success, FAIL, success, FAIL, success, FAIL, success, FAIL — within a single parallel batch and across subsequent single-call messages.
  • Every failing old_string was ≥3 lines (3, 5, and 22 lines); every succeeding one was ≤2 lines.
  • An identical old_string failed (call 1), a different edit succeeded (call 2), then the identical string failed again (call 3).
  • At the time, cmp between the file and a heredoc of the failing hunk reported identical bytes.

Post-hoc evidence from the session transcript JSONL

Extracting the actual tool_use inputs and toolUseResult records:

  1. All four failing old_strings (as received by the tool — exact bytes from the transcript) are verbatim substrings of the file content at their call time (simulated by replaying the successful edits in order over the git-committed pre-edit state).
  2. Decisive: successful Edit calls record toolUseResult.originalFile (the content the tool saw before applying that edit). The failing strings are present inside the originalFile snapshots recorded by the immediately-preceding and immediately-following successful calls. The tool refused strings its own snapshots contained.
  3. Ruled out: PreToolUse hooks (none intercept/deny for this path; the only denying hook is scoped to code suffixes), concurrent writers (the only timers do pathspec-scoped git add/commit and never touch the file), unicode/whitespace mismatch (byte-compared), non-unique matches (strings occur exactly once).

Control tests

  • The exact failing 3-line string succeeded via Edit on a byte-identical copy of the file at a different path (fresh file state) — content exonerated.
  • After an out-of-band write to the file (external python replace, which triggered the "file was modified externally" state re-sync), the same class of multi-line Edit on the real file succeeded.

Occurrence 2 (same day, different file)

A 3-line edit to another markdown file failed the same way immediately after a successful edit to that file in the same message batch. A fresh targeted Read of the hunk's lines followed by an identical retry succeeded — no content change.

Working theory + workarounds

Looks like a file-state desync after successful edits: some multi-line match path consults a stale view of the file, while ≤2-line matches and the originalFile snapshot recorder see current content. Workarounds that clear it, in increasing weight: fresh targeted Read + retry → split into ≤2-line hunks → out-of-band write (resets tracked state).

Happy to provide the extraction script used against the transcript JSONL if useful.

View original on GitHub ↗