A while back I asked Claude Code to "tidy up three spots in this function" and watched the first Edit go through cleanly, only to see the second one fail with String to replace not found in file and the third trip on String to replace ... is not unique. The spots had been there a second ago. This pattern shows up often enough that it deserves its own playbook.
I'm Masaki Hirokawa, an artist and indie developer running wallpaper and meditation apps that have crossed 50 million downloads since I started in 2014. Claude Code handles most of my day-to-day coding now, from SwiftUI refactors to MDX article swaps for my own Lab sites, and this cascading-Edit failure mode has bitten me more times than I'd like to admit. The root cause almost always reduces to one of two patterns, and the rescue path is mechanical once you spot it.
What's actually happening — Edit doesn't re-Read between calls
The thing worth internalising first is that Claude Code's Edit tool does not transparently re-Read the file after the previous Edit applied. When the model emits several Edits in one turn, the picture of the file in its head and the bytes on disk drift further apart with each successful patch.
The sequence looks like this:
- Claude reads the file once with
Read. - Based on that snapshot, it generates
old_stringvalues for Edits 1, 2, and 3 in a single batch. - Edit 1 applies and the targeted text on disk turns into its
new_string. - Edit 2's
old_stringwas built from the Read snapshot, which no longer matches the file. - If Edit 1 deleted lines that Edit 2 depended on, the
old_stringsimply isn't there anymore. - If Edit 1's
new_stringintroduced a copy of the pattern Edit 2 targets, the secondold_stringis no longer unique.
In other words, Edits are applied serially, but their old_string values were all minted against the same stale snapshot. Once you see it that way, the fixes are mostly mechanical.
Case 1: Edit 1 deletes what Edit 2 is looking for
This is the most common shape I run into. Picture a SwiftUI view with three near-identical modifier chains. Claude decides to delete one .padding(.top, 16) and then rewrite another from .padding(.top, 16) to .padding(.top, 24). By the time Edit 2 fires, the literal string Edit 2 was hunting for is already gone.
Error: String to replace not found in file.
String: .padding(.top, 16)
I trip on this constantly during AdMob-related SwiftUI work, where helper-function call sites cluster together and Claude tries to "delete the upper one and rewrite the lower one" in a single sweep.
Fix: tell Claude to slip a Read in
My habit when this fires is to reply with a one-liner: "Please Read the file again and then run only the remaining Edits." That makes Claude refresh its view of the file and regenerate the old_string values for whatever's left.
If you want to avoid the cascade in the first place, ask differently up front:
- Risky framing: "Make all three edits in one go."
- Safe framing: "Apply the edits one at a time, top to bottom. After each Edit,
Readthe file and verify the result before moving on."
The second framing slows things down, but for big refactors or structural changes it's the saner mode.
Case 2: Edit 1's new_string multiplies the pattern Edit 2 looks for
The other shape that surfaces a lot is Edit 1 inserting text that contains the very pattern Edit 2 wants to match, which inflates the file's match count.
Error: String to replace is not unique in the file.
Multiple matches found for: const handleSubmit = async () =>
For example: "Split the body of const handleSubmit = async () => and have it call an internal helper of the same shape." If Edit 1's new_string includes the same function signature on the helper side, suddenly there are two call sites where there used to be one, and any later Edit aimed at the original signature can't pick a row.
Fix: widen old_string with surrounding context until it's unique
When not unique fires, ask Claude to "regenerate old_string including the 3-5 lines around the target so it disambiguates, then retry." Edit tooling is strict literal matching, so extending the window always resolves the ambiguity.
I keep a rule in my CLAUDE.md that says "always include closing braces when editing the last function in a file, and always include neighbouring import lines when editing imports." That alone cut my not unique rate noticeably.
# Excerpt from CLAUDE.md
When using the Edit tool, make sure `old_string` is unique by including
a few surrounding lines. Especially for import statements, return blocks,
and empty braces, uniqueness is the top priority.Case 3: a formatter ran between Edits and changed the diff
This shows up less often but stings harder. If a PostToolUse hook fires a formatter after Edit 1 — Prettier, SwiftFormat, gofmt, ruff — indentation can normalise the moment the patch lands, and Edit 2's old_string (still indented the old way) no longer matches.
I lost half a day to this once when I had SwiftFormat wired into PostToolUse for every Edit in one of my iOS app projects.
Fix: don't run the formatter mid-cascade
My current setup runs the formatter only after a batch of Edits is fully settled. Practically, I moved the hook off PostToolUse per-Edit and onto Stop or SubagentStop, so the file isn't rewritten under Claude while the cascade is still in flight.
{
"hooks": {
"Stop": [
{ "command": "swiftformat ." }
]
}
}If you really want a per-Edit formatter, disable the PostToolUse hook temporarily via /hooks during the refactor and re-enable it afterwards.
Triage flow — the order I actually run through
When an Edit cascade breaks, my head walks through this list:
- Read the error: is it
not foundornot unique? - If
not found, check whether the previous Edit touched the same region. - If
not unique, eyeball whether the recentnew_stringintroduced a copy of the pattern. - If neither, run
/hooksand see if aPostToolUsehook is rewriting the file behind the scenes. - Once the cause is clear, tell Claude: "Re-Read the file, then apply the remaining Edits."
Steps 2 and 3 catch most cases. After a while you develop a feel for "how many Edits per Read is safe in this file," and you stop generating cascades in the first place.
Prevention — refactor splits that don't cascade
A few habits I lean on to keep Edit chains from collapsing:
Cap Edits per file at three per turn. If a change would need more, ask Claude to handle one file at a time, with a Read after each, before moving to the next file.
Separate structural Edits from body Edits. Changing a function signature and rewriting its body in the same turn invites the cascade. Do the signature first, confirm, then do the body in a second turn.
Push wide replacements down to Bash + sed. Renaming a symbol across ten or more sites is what sed -i 's/old/new/g' is for. I have a CLAUDE.md note that says "four or more identical replacements: use sed," and Claude picks the right tool from there.
Next step
When you hit this in real time, the single most useful thing you can say is "Re-Read the file, then continue." That clears about seven out of ten cases. The remaining three either need wider old_string context or a paused formatter hook.
I still walk into the cascade trap most weeks, but holding the mental model of "all old_strings come from one Read" makes the rescue almost reflexive. If you've been losing flow to the same error, I hope this saves a little time. Thanks for reading.