[BUG] Edit tool silently converts tabs to spaces, causing repeated match failures on tab-indented files

Open 💬 17 comments Opened Feb 19, 2026 by lukewilliamboswell

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

The Edit tool converts tab characters to spaces when the model sends old_string/new_string parameters. The file content uses real tabs (\t), the Read tool displays them as spaces, and the model reproduces them as spaces — so the Edit tool's exact-match lookup fails every time.

This is related to previously-closed #9163, #7197, #6729, #2644 — all auto-closed without resolution. Filing fresh with a detailed real-world reproduction from today.

Real-world reproduction (Go codebase, tab-indented)

During a refactoring session on a Go project (all files tab-indented per gofmt), every single Edit call failed on lines with any indentation. The session required editing 8 files. Here's what happened:

  1. Read tool displays file content — tabs render as spaces in the output, indistinguishable from actual spaces
  2. Model (Opus 4.6) constructs old_string using what it sees — spaces
  3. Edit tool does exact byte matching against the file — which has tabs
  4. Result: "String to replace not found in file" — every time

The model tried 6 consecutive Edit calls on the same file (ui/carrier.go), each failing identically. It even tried broadening the context window to include more surrounding lines — still failed because the core issue is tab-vs-space, not uniqueness.

The workaround was to abandon the Edit tool entirely and use python3 -c with explicit \t characters in string literals to perform replacements via Bash. This worked first try for all 6 remaining files.

The three-way mismatch

| Component | Tab handling |
|-----------|-------------|
| File on disk | Real \t bytes (Go, Makefiles, etc.) |
| Read tool output | Displays tabs as spaces (model can't tell the difference) |
| Edit tool matching | Requires exact byte match (tabs ≠ spaces) |

The model literally cannot succeed — it has no way to know the file uses tabs, and no way to emit tab characters even if it did.

What Should Happen?

Any of these would fix it:

  1. Best: Edit tool normalizes whitespace for matching — treat leading tabs and spaces as equivalent when finding old_string, then preserve the file's original whitespace style in the replacement
  2. Alternative: Read tool marks tabs visibly — e.g. show or \t so the model knows to emit tabs
  3. Alternative: Model can emit tab characters — if the XML parameter encoding preserved \t literals

Impact

  • High — the Edit tool is completely unusable on tab-indented files without falling back to Bash
  • Affects all Go codebases (gofmt mandates tabs), Makefiles, and many legacy codebases
  • The model burns multiple turns attempting edits before giving up, wasting context and time
  • Workaround (Bash + python3) bypasses the Edit tool entirely, losing the user-reviewable diff UX

Error Messages/Logs

String to replace not found in file.

(Repeated 6 times in succession on the same file, with progressively larger context windows — all failing for the same root cause)

Steps to Reproduce

  1. Open any Go file (tab-indented per gofmt)
  2. Read the file with the Read tool
  3. Attempt an Edit with old_string copied from the Read output
  4. Edit fails with "String to replace not found in file"
  5. Verify with cat -et that the file uses ^I (tab) characters where the Read tool showed spaces

Claude Model

Opus 4.6

Is this a regression?

Unknown — may have always been broken for tab-indented files

Claude Code Version

2.1.47

Platform

Anthropic API (Claude Code CLI)

Operating System

macOS (Darwin 23.6.0)

Terminal/Shell

zsh

Additional Information

Previous issues filed about this (#9163, #7197, #6729, #2644) were all auto-closed as duplicates of each other. None appear to have been resolved. This report includes a concrete reproduction case and analysis of the three-way mismatch between Read, Edit, and the file system.

View original on GitHub ↗

17 Comments

github-actions[bot] · 4 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/11447
  2. https://github.com/anthropics/claude-code/issues/25913
  3. https://github.com/anthropics/claude-code/issues/23741

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

Guria · 4 months ago

I observe similar in typescript project too
<img width="1039" height="652" alt="Image" src="https://github.com/user-attachments/assets/55e2d0bf-4fc4-4ae9-8c13-20c30a81eda9" />

pupperpowell · 4 months ago

This happens for me _constantly_.

⏺ Now update the two click handlers to only fire the tracking event the first time:

⏺ Update(src/lib/components/WinScreen.svelte)
  ⎿  Added 1 line, removed 1 line
      226                (window as any).rybbit?.event("Share");
      227                handleShare();
      228              } else {
      229 -              (window as any).rybbit?.event("Copy to Clipboard");
      229 +              if (!copyTracked) { (window as any).rybbit?.event("Copy to Clipboard"); copyTracked = true; }
      230                copyToClipboard();
      231                copySuccess = true;
      232                setTimeout(() => {

⏺ Update(src/lib/components/WinScreen.svelte)
  ⎿  Error editing file

⏺ Searched for 2 patterns (ctrl+o to expand)

⏺ Update(src/lib/components/WinScreen.svelte)
  ⎿  Error editing file

⏺ Read 1 file (ctrl+o to expand)

⏺ The indentation is different (tabs vs spaces). Let me use the exact content:

⏺ Update(src/lib/components/WinScreen.svelte)
  ⎿  Error editing file

⏺ Bash(cat -A /Users/george/Coding/bibdle/src/lib/components/WinScreen.svelte | sed -n '254,257p')
  ⎿  Tool use rejected with user message: just read the file again

⏺ Read 1 file (ctrl+o to expand)

⏺ Update(src/lib/components/WinScreen.svelte)
  ⎿  Added 1 line, removed 1 line
      252            aria-label="Copy to clipboard"
      253            data-umami-event="Copy to Clipboard"
      254            onclick={() => {
      255 -            (window as any).rybbit?.event("Copy to Clipboard");
      255 +            if (!copyTracked) { (window as any).rybbit?.event("Copy to Clipboard"); copyTracked = true; }
      256              copyToClipboard();
      257              bubbleCopied = true;
jopejope · 4 months ago

Corroborating this from a Windows environment where the issue is compounded by CRLF line endings — posting here since #26996 is the most complete analysis and this adds a Windows-specific angle not covered by the existing reports.

Setup

  • Windows 10 Pro, bash shell (via Claude Code VSCode extension)
  • Files: C++ source with CRLF line endings (\r\n) and tab indentation, confirmed by file(1) and cat -A
  • Model: claude-sonnet-4-6

What happened

During a routine edit session (replacing implicit pointer checks with explicit != nullptr comparisons), multi-line old_string patterns silently failed with String to replace not found in file — same symptom as described here. Shorter single-line strings with no leading whitespace succeeded immediately.

The compounding factor on Windows

On Windows, the Edit tool faces two independent sources of mismatch simultaneously:

| Component | Tab handling | Line ending |
|-----------|-------------|-------------|
| File on disk | Real \t bytes | \r\n (CRLF) |
| Read tool output | Displays tabs as spaces | Strips \r, shows \n only |
| Model's old_string | Reproduces spaces | Embeds \n only |
| Edit tool matcher | Exact byte match | Exact byte match |

Either mismatch alone can cause failure. Together they make multi-line matches on Windows tab-indented files almost certain to fail.

Inconsistency worth noting

Two files in the same session had CRLF + tabs. Multi-line edits on the first file (linux_draw.cpp) eventually succeeded; early multi-line edits on the second (linux_drawutil.cpp) failed consistently until I switched to single-line strings. I don't have a solid explanation for this inconsistency — it may relate to nesting depth (tab count per line varied), or there may be some non-determinism in how the model reconstructs indentation from the Read output.

Workaround

Avoiding leading whitespace entirely in old_string sidesteps both the tab-vs-space and CRLF problems in one move. Starting the match at a non-whitespace character (e.g. if (!foo) { rather than \t\t\tif (!foo) {) works reliably, even for multi-line matches, as long as the content after the first non-whitespace character is unique enough.

Related

This was originally filed as a separate issue (#28831) before being identified as a duplicate of this one. Noting it here for cross-reference in case the Windows/CRLF angle is useful to maintainers.

zzarbi · 4 months ago

One workaround is you can ask Claude to write its own edit script, it takes a couple tries it eventually make something that works. Better than those giant python 3 commands I've seen.

hbmartin · 4 months ago

I will cancel my subscription if this is not fixed soon since this makes CC basically unusable for me.

hbmartin · 4 months ago

I asked Claude for extensive debugging and this was how it summarized its results:

⏺ To summarize the actual root cause:

  The Edit tool is fine. It does exact byte matching, which is correct behavior. The failure is that I can't
  reliably distinguish \t from visually-identical spaces in the Read tool's output, especially when counting
  multiple levels of nesting or when indentation inside template literals might use a different style than the
  surrounding code. The "backtick template literals" pattern was a real correlation — template literal content is
  where whitespace is most ambiguous — but it's my whitespace generation that fails, not the tool.

@chrislloyd @stevenpetryk @hackyon-anthropic @ashwin-ant @fvolcic @ant-kurt @bogini @OctavianGuzu @whyuan-cc @ThariqS @ddworken @dicksontsai @catherinewu

matej21 · 4 months ago

Same issue here:

I see - there are 10 tabs before each line (the setImportOptions is already replaced on line 352,
   with 10 tabs). The Read tool showed what appeared to be 5-level indentation with tab stops but
  it's actually 10 tabs. The Edit tool's matching expects exact content. The issue is the Read tool
   displays tabs as rendered spaces, and when I copy from the Read output, I get spaces not tabs.
dkmooers · 4 months ago

+1. I use tabs across all repos I work on and this is becoming maddening.

IvanTorresEdge · 4 months ago

+1 Golang :(

aegonwolf · 4 months ago

any solutions to these? claude tries to be very creative to insert spaces and tabs now and it's horrible

quenbyako · 3 months ago

I didn't say that, but according to _\my personal divine inspiration, and ABSOLUTELY definitely not a leak of anything\_, the bug is at convertLeadingTabsToSpaces and getPatchFromContents, that calls this set of letters.

Hope it allow maintainers of this genuinely amazing product to fix the bug.

I didn't do anything wrong, don't make any lawsuits plz 👉👈

liasece · 2 months ago

+1 TypeScript!!!!

HustCoderHu · 1 month ago

Same Problem in golang, why this basic tool func bug still exists, this problem never appears in codex cli

quenbyako · 1 month ago

My divine inspiration didn't help. Shit. 🙄

iHCore · 16 days ago

I can't believe that the CC can't edit files in right way.

thomasbachem · 2 days ago

I dug into this after hitting the same thing in a tab-indented codebase. Interesting detail: in my transcripts it's not a tab→space conversion. I diffed each failed old_string against the retry that eventually succeeded, and the failed ones contain tabs – just one too many, usually on every line, even on lines the file has at column 0:

failed:    \t\t\t\t\t\tif(type === 'calendar') {
succeeded: \t\t\t\t\tif(type === 'calendar') {

No spaces anywhere, always +1, never −1. My best explanation is the Read tool's line prefix (spaces + number + tab) bleeding into the model's recalled indentation when it composes the edit later.

Numbers from six sessions: ~60 incidents, on average 2.3 tool calls until the edit finally landed, 17% with repeat failures. Hooks can't fix this either – PreToolUse doesn't fire on a non-matching old_string (#46001), updatedInput is ignored for Edit (#47853), and no post event fires (#24908). A whitespace-insensitive fallback match in the Edit tool itself would cover it: if a unique region matches ignoring leading whitespace, take the file's text as old_string and shift new_string by the same per-line deltas so the skewed indentation doesn't land in the file. Happy to share the analysis scripts.