The annoyance
I pasted the auto-format example from the hooks guide into .claude/settings.json. It is a PostToolUse hook on matcher Edit|Write, and the command is jq -r '.tool_input.file_path' | xargs npx prettier --write. The first edit landed. Prettier rewrote the file on disk. The next Edit on that same file stopped to read it again, and on a CLI older than v2.1.208 it refused. A turn that touched one file three times paid the formatter after every pass.
The usual misses were a lowercase matcher, as in lowercase matcher never fires, or a hook outside settings, as in standalone hooks file never loads. This one was firing. Each Edit left the file different from the read the next Edit still held.
What Anthropic actually says
From the official hooks guide, under “Auto-format code after edits”:
“This hook uses the PostToolUse event with an Edit|Write matcher, so it runs only after file-editing tools. The command extracts the edited file path with jq and passes it to Prettier.”
The JSON under that heading is the block I had pasted. Same page, under “Filter hooks with matchers”, the guide names a coarser event when the hook has to see the tree rather than each tool call:
“If your hook must see every file change, such as for compliance scanning or audit logging, add a Stop hook that scans the working tree once per turn.”
The auto-format section already points at the other event for one named path:
“To reformat a specific file however it changes, including when a Bash command rewrites it, use a FileChanged hook instead.”
Stop has no matcher and fires once when the turn ends. FileChanged takes literal filenames split on |, so src/index.ts is a name and *.ts is not. The working-tree listing the guide names is git status --porcelain.
The bill for that rewrite is on the tools reference, under “Edit tool behavior”. From v2.1.208 a changed file can still be edited when old_string matches the current bytes exactly. When it does not:
“In any other case, such as a stale old_string or one that matches more than once without replace_all, Claude reads the file again before editing.”
The version line on the same page:
“The relaxed handling of unread and changed files requires Claude Code v2.1.208 or later; before that, Claude Code refused any edit to a file it hadn't read in the conversation or that changed on disk after the read.”
The fix
Leave the guide’s block out when Claude will keep editing that file in the same turn. It is valid, and it is what the page shows, and it formats after every edit:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}Format once, when the turn ends. Put this under the hooks key in .claude/settings.json. Stop takes no matcher:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "git status --porcelain | awk 'length($0) > 3 { print substr($0, 4) }' | while IFS= read -r f; do npx prettier --write --ignore-unknown -- \"$f\" || true; done"
}
]
}
]
}
}The first three porcelain characters are the status, and the rest is the path, so a name with spaces survives. A rename line is old -> new. --ignore-unknown skips paths Prettier does not format. The || true is there because Prettier’s failure code is 2, and exit 2 blocks a hook, which would keep the turn alive. That contract is exit 1 does not block.
For one named file, including a Bash rewrite, and only once Claude is finished with it, use FileChanged. The matcher is the filename:
{
"hooks": {
"FileChanged": [
{
"matcher": "src/index.ts",
"hooks": [
{
"type": "command",
"command": "npx prettier --write --ignore-unknown -- src/index.ts || true"
}
]
}
]
}
}If that format still changes bytes, FileChanged can run again until the file is stable. On a file Claude is still rewriting, it is the same ping-pong.
Prove it
Confirm /hooks lists Stop and omits the PostToolUse prettier command. Ask for two edits of one file in a single turn. Prettier’s diff arrives once, after the turn.
# In-session: /hooks
# Stop is listed. PostToolUse Edit|Write prettier is not.
# Ask for two Edits of the same file in one turn.
# The file stays as Claude left it until the turn ends.
# After Stop, git diff shows one Prettier pass.
# Before v2.1.208, the second Edit is not refused for a mid-turn rewrite.Checklist
- Take the
PostToolUseEdit|Writeprettier command out if the turn will edit that file again. - Add a
Stophook that formatsgit status --porcelainpaths once per turn. - Or use
FileChangedwith a literal filename when one path must be formatted however it changes, and Claude is done writing it. - Keep the hook under the
hookskey in a settings file. A standalonehooks.jsondoes not load. - Keep matcher capitals on any
Edit|Writehook you still want.edit|writenever fires. - Keep the formatter off exit 2. Swallow Prettier’s failure so
Stopcan end the turn.
Why it fails
I treated the guide’s example as the standing setup. It formats after every Edit and Write, so the next Edit sees a file that changed since the last read. The format is right. The event is the tax.
If you want to run this with other operators rather than on your own, we are doing that in the public Skool community, and that is https://www.skool.com/navaigate.
Sources
From idea to operation
Make the next AI decision concrete.
NavAIgate helps leadership teams identify high-value AI opportunities, prove them safely and turn the winners into working systems.
