catalog / tool-behaviors
Edit fails with "String to replace not found" when old_string includes Read's line-number prefix
verified
Claude Code's
Readreturns content incat -nformat (<line>\t<text>). If a skill copies a line from that output straight intoEdit'sold_string, the leading line number and tab are not part of the file, so the match fails with "String to replace not found in file."
- Category: tool-behaviors
- Applies to: Claude.ai ❓ (see notes) | Claude Code ✅ | Claude Desktop ❓ (same
Edit, untested) - Verified on: Claude Code (Opus 4.8), 2026-06-14
- Verification tier: verified
- Severity: Hard error
Symptom
A skill reads a file, then constructs an Edit by reusing a line it just saw in the Read output. The Edit errors with String to replace not found in file. even though the text is visibly present in the file — because the copied string still carries the <line-number>\t prefix that Read prepended for display.
Cause
Read displays content with a line-number prefix (cat -n style): 1\tfirst line, 2\tsecond line, etc. That prefix is presentation only — it is not in the file. Edit requires old_string to match the file's bytes exactly, so a prefixed old_string (2\tsecond line) never matches the real content (second line). The Edit tool description warns about this ("Strip the Read line prefix (line number + tab) before matching"), but it is easy to miss when assembling edits programmatically.
Reproduction
A file whose real second line is line two, read back as 2\tline two, then fed verbatim to Edit:
$ printf 'line one\nline two\nline three\n' > /tmp/probe/edit-target.txt
Read("/tmp/probe/edit-target.txt")
→ 1\tline one
2\tline two
3\tline three
Edit(file_path=".../edit-target.txt", old_string="2\tline two", new_string="2\tline TWO edited")
→ String to replace not found in file.
String: 2 line two
Dropping the 2\t prefix (old_string="line two") matches and succeeds.
Fix
Build old_string from the file's actual content, not from Read's displayed output: strip the leading line number and the tab. The text after the first tab on a Read line is the real content.
Notes
This is distinct from, but often confused with, the prior-Read requirement (edit-requires-prior-read-same-conversation.md) — there the Read is missing entirely; here the Read happened but its formatting leaked into the edit. old_string must also be unique unless replace_all is set (../cross-platform/edit-replace-all-divergence.md). Claude.ai's view also shows line numbers, so the same trap plausibly applies to str_replace there, but this was verified only on Code.