npx claudepluginhub withholm/claude-tab-fixHook that normalizes indentation in Edit tool calls and warns Claude about the tab separator ambiguity in Read output — fixes silent failures on tab-indented files
A Claude Code hook that fixes indentation mismatches in Edit tool calls and warns Claude about the tab-separator ambiguity in Read output.
Claude Code's Edit tool matches old_string against file content as a literal string. When the model generates old_string with spaces but the file uses tabs (or vice versa), the match fails silently and no edit is applied. This is a persistent issue with Go, .templ, and other tab-indented files.
The root cause is in the Read tool: it formats output as N\t<line content>, using a tab as the line-number separator. For tab-indented files this is visually identical to the file's own indentation — the model cannot distinguish the separator tab from content tabs, so it consistently produces old_string with one extra leading tab per nesting level.
go to the right side of this website and look for releases for a bin/exe download. download that
go install github.com/WithHolm/claude-tab-fix@latest
git clone https://github.com/WithHolm/claude-tab-fix
cd claude-tab-fix
make install
Install the binary first, then install the plugin so Claude Code picks up the hook automatically:
# 1. Install the binary
go install github.com/WithHolm/claude-tab-fix@latest
# 2. Install the plugin
/plugin marketplace add WithHolm/claude-tab-fix
/plugin install claude-tab-fix@WithHolm/claude-tab-fix
The plugin registers both hooks for you — no manual config editing needed.
Install the binary, then add the hooks to your Claude Code settings.
Per-project — commit .claude/settings.json to your repo so anyone who clones it gets the hooks automatically:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"hooks": [{ "type": "command", "command": "claude-tab-fix" }]
}
],
"PostToolUse": [
{
"matcher": "Read",
"hooks": [{ "type": "command", "command": "claude-tab-fix" }]
}
]
}
}
Globally — add the same block to ~/.claude/settings.json to enable it for every project on your machine.
If the binary is not on your PATH, use the full path: $(go env GOPATH)/bin/claude-tab-fix.
PostToolUse / Read — after every Read call on a tab-indented file, the hook injects a context note reminding Claude that the N\t line-number prefix is a separator tab, not part of the file content, so old_string should have one fewer leading tab than the raw output suggests.
PreToolUse / Edit — before every Edit call, the hook:
old_stringold_string and new_string to match the fileold_string isn't found verbatim, falls back to fuzzy line-similarity matching to handle minor content driftIf the indentation already matches, the Edit hook passes through unchanged.
See FLOW.md for the full decision diagram.
| Situation | Behaviour |
|---|---|
| File doesn't exist yet | Pass through (new file creation) |
| Binary file | Pass through |
old_string has no indented lines | Pass through |
| Mixed indentation in file | Majority-wins detection |
replace_all: true | Same normalization applies |
Bash with sed/awk/perl on a tab-indented file | Allow + advisory warning injected into Claude's context |
Write overwrites a tab-indented file with space-indented content | Allow + advisory warning noting the mixed indentation risk |
The hook fires on Read (PostToolUse), Edit, Bash, and Write (all PreToolUse). Only Edit calls can be automatically corrected — Bash and Write bypass the normalization path, so the hook injects a strong suggestion to use Edit instead. A CLAUDE.md file is included in the release archive; placing it in your project root reinforces this at session start.
make build # build ./claude-tab-fix binary
make fmt # run gofmt
make install # go install
go test ./...