CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/Claude Code
Claude Code/2026-05-02Intermediate

Stop the 'old_string is not unique' Error in Claude Code's Edit Tool — A Practical Rewrite Strategy

When Claude Code's Edit tool throws 'old_string is not unique in the file', the root cause is usually that the prompt didn't guarantee a uniquely identifiable target. Here's how to design rewrites that actually stick.

claude-code129edit-tool3troubleshooting87error17fix12workflow37

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 if statements or console.log calls 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: true was needed but left at the default false

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:

  1. Read the whole file once so Claude knows how many target patterns exist
  2. Label them ("Block A", "Block B", ...) and build a uniquely-bounded old_string for each one
  3. 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 → Edit is fine
  • Replacing one entire function → Edit works if the whole function body becomes old_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"
fi

Once 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 = true

Pin 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 terminators

To 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/*.tsx

I'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.ts

If you find any, strip them with sed:

sed -i 's/\xc2\xa0/ /g; s/\xe2\x80\x8b//g' src/utils.ts

A leading UTF-8 BOM is removed with:

sed -i '1s/^\xEF\xBB\xBF//' src/utils.ts

When 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.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Claude Code2026-06-10
When git add -A Sweeps Up .bak Backups — The Quiet Trap of In-Place Fix Scripts
How .bak backups left by sed -i and homegrown --fix scripts get silently swept into production by git add -A, why it is so easy to miss, and how scoped adds and .gitignore close the gap for good.
Claude Code2026-06-09
When Yesterday's Article Bleeds Into Today's — The Stale Fixed-Name Temp File Trap
In a Claude Code automation pipeline, a fixed-name temp file kept stale content from the previous run and leaked old body text into a completely different article. Here is why the write fails silently, and how unique names plus a post-write grep check prevent it.
Claude Code2026-06-03
When git push Says Success but Nothing Lands — the Silent commit Failure in Claude Code
git push prints Everything up-to-date and exits zero, yet your changes never reach the remote. Here is why commit silently fails on a fresh sandbox clone, and how to verify a real push with SHA comparison.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →