[BUG] Claude can't edit files that use tabs for indentation
Open 💬 61 comments Opened Nov 11, 2025 by d5ve
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?
For some reason https://github.com/anthropics/claude-code/issues/9163 was closed with no resolution.
This is affecting me. Claude fails to edit files indented with tabs. PHP code in my case. It falls back to running ridiculous and fragile sed commands. See below.
What Should Happen?
The Update tool should be able to make changes to files that use tabs for indentation.
$ file application/views/user/layout/service-note-modal.php
application/views/user/layout/service-note-modal.php: HTML document, ASCII text, with very long lines (318)
Error Messages/Logs
● Read(application/views/user/layout/service-note-modal.php)
⎿ Read 25 lines
● Update(application/views/user/layout/service-note-modal.php)
⎿ Error editing file
● Bash(sed -i '88,109s/^\t\t\t\t\t\t/\t\t\t\t\t\t\t/' /home/anon/apps/portal/application/views/user/layout/service-note-modal.php && sed -i '109a\\t\t\t\t\t\t</div>' /h…)
⎿ Running…
Steps to Reproduce
Have a file with tabs.
Ask for an edit.
Watch Claude flail.
Claude Model
Sonnet (default)
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.0.37 (Claude Code)
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
iTerm2
Additional Information
_No response_
61 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
It is a duplicate with https://github.com/anthropics/claude-code/issues/9163 BUT that was closed with no resolution. So it's still an active bug.
It is probably a duplicate with https://github.com/anthropics/claude-code/issues/10332 BUT that was closed as a duplicate for the other one. So it's still an active bug.
It is probably a duplicate with https://github.com/anthropics/claude-code/issues/753 BUT that was closed mentioning a fix applied months ago. I am still seeing the issue today so it's still an active bug.
This is ridiculous!
Check out this command!
I'm facing similar problems. If it's of any help, I asked claude to tell me what the problem is:
To which it replied:
I'm on Windows 11, in my case it's c++ and I'm using tabs instead of spaces for indentation. Line endings on Windows are CRLF and Claude mentions that its tooling was designed for LF so that's another problem.
Same here, using WSL/Windows 11. This is a serious issue for people who are affected.
This is still a problem and a massive time/token waster. Virtually every time I kick off an implementation after planning I'm hit with the dreaded
Error editing file, followed by 2 minutes of claude trying to figure out how to insert a few lines of code in a file.I still run into this issue.
I have had some success with periodically adding the following to the chat.
In most cases, the next few file edits do work.
I ran into the same problem and I added the following instruction to CLAUDE.md, which taught Claude how to not run into the issue:
This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.
The issue is still occurring.
Extremely annoying. Anybody knows how to fix it?
Any idea what's the fix?
Has anyone been able to fix this? From Claude:
I'm not going to sit here and approve every sed command. And just now Claude said the edit was too complex for sed, and it needed to write a perl script to apply the edits.... A PERL SCRIPT TO EDIT A FILE.
Codex does not have this problem.
This happens everyday. I'm not paying for tokens to fix the failed apply due to indent mismatch. Please fix this.
I (probably) have the same issue. Quote from Claude:
Still hitting this regularly on macOS (darwin 25.2.0, Opus 4.6). The model narrates its workaround strategy to the user — "The Read tool shows them as spaces, but Edit needs actual tabs. Let me use a simpler match." — burning context window and user attention on every retry.
Happens multiple times per session when editing tab-indented files (PHP, Makefiles, Go, etc.). The model eventually succeeds but wastes 2-3 attempts each time.
eg from terminal:
`⏺ Bash(cat -vet /Users/jez/Documents/...... |
sed -n '276,291p')
⎿ ^I^I^I^Iif (bindings.CONTENT_INDEX) {$
^I^I^I^I^Itry {$
^I^I^I^I^I^Iconst upsertedPage = await getPageByUrl(bindings.DB, pageUrl);$
… +13 lines (ctrl+o to expand)
⏺ 6 levels of tab indentation. I need to write the replacement with matching tabs. Let me write the
whole block using a Write approach — actually, let me use a single targeted edit with the exact
unique string.
⏺ Read 1 file (ctrl+o to expand)
⏺ The issue is that Edit is converting my tabs to spaces. Let me use Bash with sed for this specific
edit since the tab handling is problematic.
`
Also having this issue on Windows. Will this ever be acknowledged?
Ran into this issue multiple times as well, cf Claude reasoning:
Then it used a combination of Read + Write tools to completely overwrite the file.
This appears to be something that won't be fixed? Rather disappointing
Agreed that this is super annoying. I've got to sit here and watch this thing create a python script or exec sed just to edit a file, approving each command? Come on...
At cc's suggestion, I tried adding this to my CLAUDE.md but it doesnt help much
- If the Edit tool fails with "String not found" on tab-indented files, try a shorter or differently-bounded match string (e.g., spanning lines with mixed indentation levels, or starting from a more unique position) before reaching for any shell tool.This might have helped:
Also experiencing this issue - only started in the last week - running on MacOS Tahoe.
Adding notes/instructions in claude.md and memory.md have zero effect, I have to prompt several times within a session that the file uses tabs. Experiencing the same sequence of an error reading/editing the file, then falling back to sed to replace text.
Huge time suck + token waste to sit there manually approving sed commands (which fail half the time) or cancelling whole plans/edit sequences to restart with yet another reminder that the file uses tabs.
Claude itself:
Since it seems unlikely to be fixed any time soon, I've researched our options.
Edit tool built-in alternative that shouldn't fail is Write tool, but it is token-heavy, it rewrites the whole file.
Sed is more efficient, but we have to manually approve it, Claude doesn't prompt to pre-approve it.
But we can pre-approve it by adding sed to the config
And save instructions to memory, so it doesn't repeatedly waste time on Edit tool or try to use node or python instead of sed
Same here, same here. I'm almost giving up and using PHPStorm to commit the code as it auto format the code before committing. I know most people uses spaces nowadays but I find it really sad to have a whole key on the keyboard made specifically for tabulation and having to revert to spaces for the sake of AI.
Please, from dev to dev, help us!
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 here. The strange thing is that this started this week. I had never seen Opus 4.5 do this, but it seems that 4.6 gets very confused with tabs. In my scenario, it's HTML and Typescript files. It even creates a Python script to try to inject the tabs. Insane.
Pleas fix this stupid bug. Took like 5 minutes to fix tabs! And in the end, i had to fix it manually!
```● Update(src/routing/pdf/orderLinesReportPdf.ts)
⎿ Error editing file
● Bash(cat -A /home/edgars/hub/aiz-server/src/routing/pdf/orderLinesReportPdf.ts | sed -n '1408,1415p')
⎿ ^I^I^I^I^I^I]$
^I^I^I^I^I]$
^I^I^I^I}$
… +5 lines (ctrl+o to expand)
● Update(src/routing/pdf/orderLinesReportPdf.ts)
⎿ Error editing file
● Bash(python3 - << 'PYEOF'
with open('/home/edgars/hub/aiz-server/src/routing/pdf/orderLinesReportPdf.ts', 'r') as f:…)
⎿ '\t\t\t\t\t]\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst pdfDoc'
Found: True
"\n\t\t\t\t\t\t\t{text: totalWbSkaits, style: 'cellBold', fillColor: 'silver'}\n\t\t\t\t\t\t]\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst pdfDoc = pdf"
● Bash(python3 - << 'PYEOF'
with open('/home/edgars/hub/aiz-server/src/routing/pdf/orderLinesReportPdf.ts', 'r') as f:…)
⎿ Done
● Let me verify the result looks correct:
● Searched for 1 pattern, read 1 file (ctrl+o to expand)
● Bash(cat -A /home/edgars/hub/aiz-server/src/routing/pdf/orderLinesReportPdf.ts | sed -n '1300,1310p')
⎿ ^I^I^I^I^I^I^Ireturn rows$
^I^I^I^I^I^I})$
^I^I^I^I^I] as TableCell[][]$
… +8 lines (ctrl+o to expand)
● Searched for 1 pattern (ctrl+o to expand)
● The new else if is at 1 tab instead of 2. Let me fix the indentation:
● Bash(python3 - << 'PYEOF'
with open('/home/edgars/hub/aiz-server/src/routing/pdf/orderLinesReportPdf.ts', 'r') as f:…)
⎿ Found: True
Found2: True
Done
● Now add DestCountryCodeFin to the landscape condition:
● Read 1 file (ctrl+o to expand)
● Update(src/routing/pdf/orderLinesReportPdf.ts)
⎿ Error editing file
● Bash(python3 - << 'PYEOF'
with open('/home/edgars/hub/aiz-server/src/routing/pdf/orderLinesReportPdf.ts', 'r') as f:…)
⎿ Found: False
'= OrderLinesReportType.OakWb) {\n\t\t\tconst mult = (line: OrderLineModel) => {\n\t\t\t\tif'
● Bash(python3 - << 'PYEOF'
with open('/home/edgars/hub/aiz-server/src/routing/pdf/orderLinesReportPdf.ts', 'r') as f:…)
⎿ Line 1488: '\t\t\t\t\t\targs.data.report === OrderLinesReportType.OakWb\n'
● Bash(python3 - << 'PYEOF'
with open('/home/edgars/hub/aiz-server/src/routing/pdf/orderLinesReportPdf.ts', 'r') as f:…)
⎿ Line 1486: '\t\t\t\t\t\t) &&\n'
Line 1487: '\t\t\t\t\t\t\targs.data.reportMode === OrderLinesReportMode.Full) ||\n'
Line 1488: '\t\t\t\t\t\targs.data.report === OrderLinesReportType.OakWb\n'
… +7 lines (ctrl+o to expand)
● Bash(python3 - << 'PYEOF'
with open('/home/edgars/hub/aiz-server/src/routing/pdf/orderLinesReportPdf.ts', 'r') as f:…)
⎿ Found: True
Done
● Now let me verify the final state of the key sections:
● Read 1 file (ctrl+o to expand)
● Bash(cat -A /home/edgars/hub/aiz-server/src/routing/pdf/orderLinesReportPdf.ts | sed -n '1408,1420p')
⎿ ^I^I^I^I^I^I]$
^I^I^I^I^I]$
^I^I^I^I}$
… +10 lines (ctrl+o to expand)
● The body inside the new else if is under-indented by one tab. Let me fix:
● Bash(python3 - << 'PYEOF'
with open('/home/edgars/hub/aiz-server/src/routing/pdf/orderLinesReportPdf.ts', 'r') as f:…)
⎿ Block body: lines 1414 to 1461 (0-indexed)
First body line: '\t\tconst orderLines = await orderLineReportDestCountryCodeFin(args.data, context.query)\n'
Last body line: '\t\t}\n'
Done
● Bash(cat -A /home/edgars/hub/aiz-server/src/routing/pdf/orderLinesReportPdf.ts | sed -n '1412,1465p')
⎿ ^I^I}$
^I^I} else if (args.data.report === OrderLinesReportType.DestCountryCodeFin) {$
^I^I^Iconst orderLines = await orderLineReportDestCountryCodeFin(args.data, context.query)$
… +35 lines (ctrl+o to expand)
● There's one extra \t\t} — the content = {} closing is at 2 tabs but should be 3. Let me check and fix:
● Bash(python3 - << 'PYEOF'
with open('/home/edgars/hub/aiz-server/src/routing/pdf/orderLinesReportPdf.ts', 'r') as f:…)
⎿ 1458: "\t\t\t\t\t\t\t{text: line.qtyOrigOrd, style: 'cell'}\n"
1459: '\t\t\t\t\t\t])\n'
1460: '\t\t\t\t\t]\n'
… +12 lines (ctrl+o to expand)
● Bash(python3 - << 'PYEOF'
with open('/home/edgars/hub/aiz-server/src/routing/pdf/orderLinesReportPdf.ts', 'r') as f:…)
⎿ Line 1462: '\t\t}\n'
Fixed to: '\t\t\t}\n'
Done
● Now let me also check there's an empty line issue (the blank line with just \t tab):
● Bash(python3 - << 'PYEOF'
with open('/home/edgars/hub/aiz-server/src/routing/pdf/orderLinesReportPdf.ts', 'r') as f:…)
⎿ 1413: '\t\t} else if (args.data.report === OrderLinesReportType.DestCountryCodeFin) {\n'
1414: '\t\t\tconst orderLines = await orderLineReportDestCountryCodeFin(args.data, context.query)\n'
1415: "\t\t\tif (orderLines.length === 0) return res.status(404).json(new UserInputNotExistError('orderLineIds'))\n"
… +5 lines (ctrl+o to expand)
● Line 1416 has a \t\n (tab + newline) instead of just \n. Let me clean that up and do a final verification:
● Bash(python3 - << 'PYEOF'
with open('/home/edgars/hub/aiz-server/src/routing/pdf/orderLinesReportPdf.ts', 'r') as f:…)
⎿ Done
● Now let me do a final read to verify the complete block looks correct:```
Basically AGI
how can it happen in the best coding agent tool? LMAO
happening for me as well
Same problem here with Sonnet 4.6/Claude Agent SDK through GitHub Copilot in VS Code and Biome/tab by default formatted TypeScript files in Windows. No problem for Opus 4.6 so far.
Same problem on my end with the latest version of claude.
Lately, I've often wondered why Claude uses
sedso much... but then I just kind of ignored it.I was about to buy a Max plan today, but when Claude wrote a huge Python script just to make a small update, I started to wonder what was going on and found this issue.
It's unbelievable that this problem even exists.
Have you tried writing your code yourself?
for a fix:
ive created a small tool that converts indentation to spaces before it reaches claude and back if file is tab indented.. so essentially claude always reads spaces but the tool does the actual conversion:
you add it as a pre-edit hook, and it works really well :)
https://github.com/WithHolm/claude-tab-fix
ive not noticed this happening anymore, or if it does maybe its much less frequent so one of the recent fixes for new lines etc maybe helped?
Oh well, at least 5.3-codex or 5.4 on extra-high consistently produce notably higher quality than CC, even if they are rather slow.
Guess I'll just use CC for code review.Nevermind! Seems reviews also do not actually work.
@amorriscode @rboyce-ant @dhollman @sid374 @Masa1984a @amorriscode
I've seen that it can handle "simple" updates, but if it comes to code that is edited along multiple nestings it struggles some days.. one day it will work as expected, but other days I wonder if it eats crayons
Still an issue
Still an issue for me with Sonnet 4.6 on macos: 2.1.81 (Claude Code).
Hitting this constantly as a WSL2 user with source code on an NTFS mount.
My codebase is Visual Studio C# — true Windows projects that can't be put through Unix normalisation.
The visible symptom is a stream of "error editing" failures followed by silent fallbacks to Python or sed to perform the edit instead. My biggest frustration with this workaround is that it bypasses the diff UI entirely — I struggle to review what actually changed before accepting it, which defeats much of the point of using Claude Code interactively.
I haven't dug into the internals myself, but the frequency of failures on my setup makes me suspect that NTFS mounts may compound the tab-matching issue further.
Still having this issue when handling typescript and svelte files
/tmp/issue-11447-answer.md
I can confirm that https://github.com/anthropics/claude-code/issues/11447#issuecomment-3694300590 solves the problem. Maybe it should be added to the system prompt?
Root Cause
The failure stems from a specific design choice in the compact line prefix format used by the Read tool (
FileReadTool).The Ambiguous Separator
addLineNumbersinsrc/utils/file.tshas two modes. In the default compact mode (enabled byisCompactLinePrefixEnabled()), each line is formatted as:The separator between the line number and the file content is a literal
\t(tab character). This is the root of the ambiguity. If the actual file uses tab-based indentation (e.g.,\t\tfunction foo()), the Read tool output becomes:All three
\tcharacters are visually identical. The model cannot distinguish the line-number delimiter from the file's own indentation. This problem compounds at multiple nesting levels and is worst inside template literals, where indentation may intentionally mix styles.Why
convertLeadingTabsToSpacesDoesn't Help HereThe function
convertLeadingTabsToSpacesexists insrc/utils/file.ts:But it is not applied in the
FileReadTool's formatting path.formatFileLinesinFileReadTool.tscallsaddLineNumbersdirectly on raw file content:convertLeadingTabsToSpacesis only used inFileEditTool/utils.tsfor the display diff (not for the model-facing content), and in the terminal UI renderer (HighlightedCode/Fallback.tsx) — never in what the model sees when reading a file. [4](#0-3)---
Proposed Fixes
There are two viable approaches:
---
Fix 1 (Surgical): Change the Compact Separator from
\tto→(U+2192)The minimum-change fix is to replace the tab separator with the arrow character
→(U+2192), which is already used in the legacy format and is unambiguously non-whitespace. This is a one-line change inaddLineNumbers:Change
\tin the template literal to\u2192. This works because:stripLineNumberPrefixalready handles both→and\tvia the character class[\u2192\t]— no change needed there: [6](#0-5)queryHelpers.tsdownstream stripping logic also usesstripLineNumberPrefix, so it also requires no change: [7](#0-6)FileEditTool/prompt.tswould need a minor text update (changing'line number + tab'to describe the→separator): [8](#0-7)This fix imposes no risk to
FileEditToolmatching becauseold_stringis always matched against the raw file content (not the Read tool's formatted output). ThenormalizeFileEditInputfunction inFileEditTool/utils.tsreads the actual file for comparison, independent of the separator character: [9](#0-8)---
Fix 2 (Deeper): Apply
convertLeadingTabsToSpacesInsideformatFileLinesA more thorough fix normalizes leading tabs before adding line numbers in
FileReadTool.ts:Change
formatFileLinesto callconvertLeadingTabsToSpaces(file.content)before passing toaddLineNumbers. This way the model always sees(spaces) for indentation, never\t, and the\tseparator after the line number remains the only tab on each line.However, this fix requires a corresponding change to
normalizeFileEditInputinFileEditTool/utils.ts. Currently, when the model copiesold_stringfrom the Read output it sees space-indented content, but the actual file has tabs — so the exact-match check will fail: [10](#0-9)A new fallback would need to be added after the desanitize fallback: normalize leading tabs to spaces in both
fileContentandold_stringto find the match location, then reconstruct the actual (tab-containing)old_stringfor the edit application.Apparently the above behavior is entirely controlled by a backend experiment configuration which explains the inconsistency we're seeing.
And makes it all the more maddening that there is no config option here since conditional path is literally already implemented.
Claude Code leaking its source code may be the best thing this year to happen to Claude Code
I remember 20 years ago saying to a colleague, when he talked about "keeping spaces for compatibility": Hey, we’re past the dark times already — modern tools work fine with semantic things like tabs.
And here we are, 20 years later, in 2026 — damn, AI still cannot work with tabs. What’s next? Will it break files without a newline at the end? Or will we have to add a carriage return manually after each line AI writes?
It's ridicolous the issue is staying open for half a year.
Seems silly, but I was able to fix this issue with one line addition to APPEND_SYSTEM.md
Can we get this known problem with a known fix applied? It's making VSCode claude code impossible to use
i posted a plugin i made earlier that fixes this. after a lot of different reiterations, i ended up both trying to auto-indent any posted code using the file known indentation AND instruct claude preciely of how to read the output from its read functionality each time it reads as a context clue. using it on a go project that mixes both tabs and spaces, it works almost flawlessly now. i haven't had any issues in the last couple weeks: https://github.com/WithHolm/claude-tab-fix you can download the build exe, build it from go source and install the plugin to get the correct hooks
I can't believe someone at the most valuable AI company on planet earth has made an objectively ludicrous decision such as using the most important and prevalent character in code -- tab -- as a crucial delimiter in a toolchain that deals with... code. In a day and age where indentation-sensitive languages are extremely prevalent (Python). How is this even possible?
For me
v2.1.81 works fine (not perfect) with Edit, but latest v2.1.1xx - better to use
sedcommand not Edit tool..It's been 6 months, can't believe this bugs still exists.
WTF Is wrong with this company ? This is a no brainer bug that can easily be fixed ? no? why do they not fixing it ? Retardation?
Same Problem. Very Disgusting.
Same Problem, why this basic tool func bug still exists, this problem never appears in codex cli
It's a feature designed to drive token usage, not a bug
Guys dont worry, AI will take our job, except for code bases with tabs.