[BUG] Edit tool silently converts tabs to spaces, causing repeated match failures on tab-indented files
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:
- Read tool displays file content — tabs render as spaces in the output, indistinguishable from actual spaces
- Model (Opus 4.6) constructs
old_stringusing what it sees — spaces - Edit tool does exact byte matching against the file — which has tabs
- 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:
- 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 - Alternative: Read tool marks tabs visibly — e.g. show
→or\tso the model knows to emit tabs - Alternative: Model can emit tab characters — if the XML parameter encoding preserved
\tliterals
Impact
- High — the Edit tool is completely unusable on tab-indented files without falling back to Bash
- Affects all Go codebases (
gofmtmandates 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
- Open any Go file (tab-indented per
gofmt) - Read the file with the Read tool
- Attempt an Edit with
old_stringcopied from the Read output - Edit fails with "String to replace not found in file"
- Verify with
cat -etthat 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.
17 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
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" />
This happens for me _constantly_.
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
\r\n) and tab indentation, confirmed byfile(1)andcat -AWhat happened
During a routine edit session (replacing implicit pointer checks with explicit
!= nullptrcomparisons), multi-lineold_stringpatterns silently failed withString 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
\tbytes |\r\n(CRLF) || Read tool output | Displays tabs as spaces | Strips
\r, shows\nonly || Model's
old_string| Reproduces spaces | Embeds\nonly || 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_stringsidesteps 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.
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.
I will cancel my subscription if this is not fixed soon since this makes CC basically unusable for me.
I asked Claude for extensive debugging and this was how it summarized its results:
@chrislloyd @stevenpetryk @hackyon-anthropic @ashwin-ant @fvolcic @ant-kurt @bogini @OctavianGuzu @whyuan-cc @ThariqS @ddworken @dicksontsai @catherinewu
Same issue here:
+1. I use tabs across all repos I work on and this is becoming maddening.
+1 Golang :(
any solutions to these? claude tries to be very creative to insert spaces and tabs now and it's horrible
I didn't say that, but according to _\my personal divine inspiration, and ABSOLUTELY definitely not a leak of anything\_, the bug is at
convertLeadingTabsToSpacesandgetPatchFromContents, 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 👉👈
+1 TypeScript!!!!
Same Problem in golang, why this basic tool func bug still exists, this problem never appears in codex cli
My divine inspiration didn't help. Shit. 🙄
I can't believe that the CC can't edit files in right way.
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_stringagainst 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: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),updatedInputis 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 asold_stringand shiftnew_stringby the same per-line deltas so the skewed indentation doesn't land in the file. Happy to share the analysis scripts.