If you've spent more than a few days with Claude Code, odds are you've already met Error: old_string is not unique in the file. You hand Claude a perfectly reasonable three-line snippet, the Edit tool refuses it because the same pattern appears elsewhere, and Claude burns tokens retrying with slightly different shapes. Sound familiar?
I run a content pipeline that has Claude Code editing across four sites in parallel, and there was a stretch where this error felt like the dominant failure mode. The fix turned out to have nothing to do with the tool itself — the issue was that I was giving Claude too little context to pick a unique edit target. Here's the playbook I now lean on.
Why the error fires in the first place
The Edit tool replaces exactly one occurrence of old_string in the file. Unless you set replace_all: true, the moment more than one match exists, the tool refuses to act because it can't safely guess which match you meant. That's a feature, not a bug — it's the safety rail that prevents accidental mass-replacement of look-alike snippets.
So the error isn't the tool being strict. It's a signal that the chosen old_string wasn't actually unique. In most real codebases, the culprit is one of these:
- Several
ifstatements orconsole.logcalls that look identical line-for-line - Boilerplate lines like
import { useState } from "react"or default useState initializers - Repeated phrases inside frontmatter or top-of-file comment blocks
- An intentional bulk replacement where
replace_all: truewas needed but left at the defaultfalse
The remedy in every case comes down to: make the old_string longer, or pull in surrounding context until it identifies exactly one location.
Pattern 1: Include 1–2 lines of surrounding context
The simplest and most effective approach is to widen old_string so it covers a uniquely identifiable block. Imagine a file with two near-identical setLoading(false) calls.
// The block you want to update
if (response.ok) {
setData(await response.json());
setLoading(false);
}
// Another similar block elsewhere
if (cached) {
setData(cached);
setLoading(false);
}Sending setLoading(false) alone is guaranteed to fail. The fix is to ship the entire surrounding block as old_string.
// old_string (now uniquely identifiable)
if (response.ok) {
setData(await response.json());
setLoading(false);
}
// new_string
if (response.ok) {
setData(await response.json());
setLastFetched(Date.now());
setLoading(false);
}Unless your file has a wildly repetitive structure, that block will only match once. If you want Claude to default to this approach, add a single line to your CLAUDE.md: "When using Edit, always include 1–2 lines of surrounding context to make old_string unique." That one rule cut my own occurrence rate dramatically.
Pattern 2: Know when replace_all is the right call
Some edits are genuinely "rename every occurrence in this file" operations — variable renames, import path migrations, that kind of thing. For those, replace_all: true is exactly what you want, and you need to call it out explicitly when prompting Claude.
// Example prompt for Claude
// "In the following file, rename every occurrence of `oldVarName` to `newVarName`.
// Use replace_all: true so the entire change happens in a single Edit call."The catch is that replace_all is blunt — it touches every match, including incidental ones inside string literals or comments. A trailing // TODO: rename oldVarName comment will get rewritten too. Reserve replace_all for tokens that are unlikely to collide accidentally (typically variable names, not English words). Running grep -n first to preview the matches takes ten seconds and saves a lot of grief.
Pattern 3: Split the work when one Edit can't cover it
If you have several similar blocks that each need a different update, multiple Edit calls are the right shape — not a single clever one. Asking Claude to "use a regex to handle all of them" doesn't help, because the Edit tool only accepts exact-string matching.
The prompt design that works best for me is:
- Read the whole file once so Claude knows how many target patterns exist
- Label them ("Block A", "Block B", ...) and build a uniquely-bounded
old_stringfor each one - Apply the edits one at a time; if any of them fails, stop and inspect before continuing
When you front-load this as a "change plan" before any Edit runs, the agent's trial-and-error drops sharply. I've baked this into CLAUDE.md as: "Before making multi-location changes, present the change plan as a bulleted list, then run Edits in order." That single instruction is responsible for roughly an 80% drop in not-unique errors in my own workflow.
Pattern 4: Reach for Write when the rewrite is large
Once a refactor changes more than ~30% of a file, you're better off using Write to replace the whole file than chaining many Edit calls. Edit is built for surgical changes. Structural rewrites — adding or removing functions, reordering sections — fight against its assumptions.
A rough decision rule:
- Net change of ±10 lines or so →
Editis fine - Replacing one entire function →
Editworks if the whole function body becomesold_string - Anything that restructures the file → use
Write
When you switch to Write, remember the protocol: you must Read the file first. Claude Code blocks Write calls on existing files that haven't been read in the current session, and that's intentional — it's the safeguard against blindly stomping on contents.
A note on file size and search performance
If you find yourself hitting this error consistently in a single file, that file might also be too large for the Edit tool to feel comfortable in. Once a TypeScript or Python file climbs past about 800–1,000 lines, repeated patterns become statistically inevitable, and any short old_string is unlikely to be unique. In that situation, the right fix is usually structural rather than tool-level: split the file into smaller modules, separate concerns, or extract repeated logic into helper functions. The Edit errors will quietly disappear once the file size drops, because the unique-context window naturally shrinks with it.
A related habit that helps: keep a short note at the top of long files listing the sections inside (e.g. // 1) Auth handlers 2) Cache utils 3) HTTP client). It costs nothing to maintain, and it gives Claude a navigation map so it can target the right section without scanning the whole file looking for landmarks.
Recovering from the error in real time
When the error does appear mid-session, this is the fastest recovery path:
# Step 1: Confirm where the pattern actually appears
grep -n "the literal string" path/to/file.ts
# Step 2: If it shows up more than once, capture the surrounding lines for context
sed -n '10,20p' path/to/file.ts
# Step 3: Re-prompt Claude: "Include the 2 lines above and below as part of old_string"In Claude Code's chat, paste the grep output back to Claude and say something like: "Of these matches, I want to edit the one at line X. Build an old_string that includes 2 lines of context above and below, then call Edit." That nearly always succeeds on the first retry. Just handing Claude the error message alone tends to trigger another guess; pairing it with the actual grep output gives Claude the structural information it needs.
What about other AI editors?
One small comfort: this kind of error is not specific to Claude Code. Cursor, Aider, and similar agent-style editors all enforce some form of unique-target safeguard for in-place edits. The exact wording of the error differs, but the underlying constraint is identical — automated edits need a single, identifiable anchor in the file. So the habits you build for Claude (context-bracketed old_string, change plans for multi-location edits, switching to a full rewrite for structural changes) carry over to other tools cleanly. Time invested in this skill is durable.
Wrapping up
The old_string is not unique error isn't really about the Edit tool being picky — it's a quiet reminder that the request wasn't pinned to a single, identifiable spot. The smallest useful change you can make today is to include 1–2 lines of context in every old_string you (or Claude) build. If you're using Claude Code seriously, dropping that rule into CLAUDE.md is a one-line investment that pays for itself within an hour.
What the error really means
The internals of Edit are surprisingly simple: take old_string, locate it inside the file, and replace it with new_string. The match is exact, so a single space, a stray newline, or one different character is enough to return not found.
# Conceptually, this is what Edit does internally
if [[ "$file_content" == *"$old_string"* ]]; then
# apply the replacement
else
echo "Error: String to replace not found in file"
fiOnce you understand that, every failure case falls into one of three buckets: the file changed under Claude's feet, Claude tried to match a string it didn't actually see, or there are hidden characters you can't perceive in your editor. Let's take them one at a time.
Case 1: Format-on-save rewrote the file behind your back
This is by far the most common scenario. VS Code's editor.formatOnSave, Prettier, ESLint's --fix, Black for Python, gofmt — any tool that rewrites a file when you press save can move the file out from under Claude.
Suppose Claude reads this:
function calculateTotal(items: Item[]) {
return items.reduce((sum, item) => sum + item.price, 0);
}Then you save the file and Prettier converts the four-space indent to two:
function calculateTotal(items: Item[]) {
return items.reduce((sum, item) => sum + item.price, 0);
}Claude tries to find the four-space version, doesn't, and the Edit fails. The frustrating part is that Claude has no way to detect that the file changed; from its perspective, the cached Read result is still authoritative.
Fix
The cleanest fix is to ask Claude to re-read the file before editing. A short prompt is enough:
The file may have been reformatted on save. Please call Read again
to fetch the current contents before applying the Edit.For longer-term stability, I prefer to disable editor.formatOnSave for the workspace I'm pairing on with Claude Code, or to maintain a dedicated VS Code profile with formatters turned off. Trying to live with format-on-save and aggressive Claude editing in the same session is a fight you'll keep losing. Another approach I've seen work well on small teams is to move formatting to a pre-commit hook rather than save-time, so Claude and the formatter don't step on each other during an active session.
Case 2: Tabs and spaces look identical to your eyes
If your editor renders a tab as four spaces, you can't tell them apart visually. When Claude reads a file that uses tab indentation, it sees actual \t characters; if you assume "those are spaces" and write your prompt accordingly, you can lead Claude into the same wrong assumption.
cat -A removes all ambiguity:
$ cat -A src/utils.ts | head -3
function calculate() {$
^Ireturn 42;$
}$^I is a tab; $ marks the end of line. If indentation is mixed, simply telling Claude "this file uses tabs for indentation" is often enough to unblock the next Edit. For repositories you'll work in for months, drop in an .editorconfig file at the root. Claude reads it on file open and adapts automatically — well worth the two minutes it takes to add. A minimal example:
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = truePin this once and most of the indentation-related Edit failures across a project disappear.
Case 3: Line endings are mixed (CRLF vs LF)
If you collaborate with developers on Windows, or your repository's git settings have drifted, you can end up with files where some lines end in \r\n and others in \n. Claude internally normalises to LF, so Edits against CRLF files start to misbehave.
Check with file:
$ file src/components/Button.tsx
src/components/Button.tsx: ASCII text, with CRLF line terminatorsTo standardise on LF for the whole repository, drop a .gitattributes at the root:
* text=auto eol=lf
Then re-normalise existing files:
git rm --cached -r .
git reset --hard
# Or, file by file:
dos2unix src/components/*.tsxI've been bitten by this on every cross-platform project I've worked on. Once .gitattributes is in place, Claude Code's Edit tool gets noticeably more reliable. If you're on Windows and reluctant to change global git settings, you can scope the conversion to just the files you actively edit with Claude — but in my experience, normalising the entire repo is less mental overhead in the long run.
Case 4: Invisible characters (zero-width spaces, BOM)
Less common, but worth checking when the first three cases don't apply. Code pasted from web pages, screenshots that went through OCR, or strings copied through Markdown can pick up zero-width spaces (U+200B), non-breaking spaces (U+00A0), or a UTF-8 BOM at the start of the file.
# Detect invisible characters
$ grep -P "[\x{00A0}\x{200B}-\x{200D}\x{FEFF}]" src/utils.tsIf you find any, strip them with sed:
sed -i 's/\xc2\xa0/ /g; s/\xe2\x80\x8b//g' src/utils.tsA leading UTF-8 BOM is removed with:
sed -i '1s/^\xEF\xBB\xBF//' src/utils.tsWhen importing files from outside the repo — a code sample from a tutorial, a snippet from a Slack message, anything that passed through a rich-text layer — I run file against them as a habit. It's a five-second check that saves a long debugging session later.
Case 5: Claude tried to use placeholder ellipses
Sometimes Claude includes a placeholder like // ...existing code... inside old_string. That can never match because the placeholder is in Claude's mental model, not in the file. The fix is explicit:
Do not use ellipsis placeholders ("// ...existing code...") in old_string.
Always copy a literal substring that exists verbatim in the file.If Claude keeps slipping back into placeholder mode, ask for the entire function or block as old_string instead of a small snippet. Counter-intuitively, longer strings tend to fail less often: a complete function body is usually unique within the file, so there's nothing for Claude to misremember. If the function is too large to fit comfortably in a single Edit call, breaking it into two sequential Edits — one for the top half, one for the bottom — is often more reliable than wrestling with surgical replacements in the middle.
When none of the above works
The five cases cover the vast majority of failures, but if you're still stuck, fall back in this order. First, ask Claude to /clear and start fresh from a Read. Second, switch from Edit to a Write of the entire file — once you've burned ten Edit attempts, a single Write is faster and cleaner. Third, look one level wider: failing Edits sometimes correlate with broader environmental issues, in which case Claude Code's /doctor self-diagnosis command can surface configuration or permission problems that aren't visible from the surface. If your shell tools are also misbehaving, the Claude Code Bash tool execution error fixes often share a root cause — encoding, permissions, or a stale environment variable.
Your next step
If any of the three core cases (format-on-save, line endings, invisible characters) sound familiar, run cat -A filename | head -20 once on the file in question. That single command exposes about 80% of the surprises. Once you start treating the file system as the source of truth and Claude as a careful collaborator that needs an accurate snapshot, the Edit tool stops feeling fragile.
For my own projects, simply committing .editorconfig and .gitattributes cut Edit failures roughly in half. It's a quiet investment with a real payoff. May your next session flow more smoothly than the last.