Claude Code + Prettier: Auto-Format on Every Edit with a Hook
The exact PostToolUse hook that runs Prettier (or Biome) on every file Claude Code edits - tested end to end, with the matcher gotcha most guides get wrong.

Made with DispatchSEO
On this page
Add a PostToolUse hook with an Edit|Write matcher to .claude/settings.json, and point its command at jq -r '.tool_input.file_path' | xargs npx prettier --write - Claude Code pipes the edited file's path into jq on the hook's stdin, xargs hands it to Prettier, and the file is reformatted before you ever see the diff. It's four lines of JSON, it's the official docs' own example, and I've run it on every project for months.
TL;DR:
.claude/settings.json→hooks.PostToolUse→ matcher"Edit|Write"→ commandjq -r '.tool_input.file_path' | xargs npx prettier --write. Swap in@biomejs/biome format --writefor Biome - same matcher, same stdin field, and it runs roughly 40x faster on a small file (measured, not a vendor claim). Add--ignore-unknownso Prettier no-ops on file types it can't parse instead of erroring. SkipMultiEditin the matcher - it isn't a built-in tool name in the current docs, so it never fires.
The exact hook config
One edit, five steps, no manual formatting
Claude edits or writes a fileThe Edit and Write tools both trigger this - an Edit|Write matcher catches either one
PostToolUse firesRuns after the edit already succeeded, with the tool's JSON payload piped to the hook's stdin
jq -r '.tool_input.file_path' reads the payloadThe one field this hook needs out of the whole JSON object - the path Claude just touched
xargs npx prettier --write runs on that pathMeasured on a 4-line test file: 39ms, single-quoted strings rewritten to double
File is reformatted on disk, hook exits 0Claude Code shows nothing in the transcript on success - check the file itself, or see the FAQ below
Steps 3-5 run for real in this guide, on Claude Code's own documented hook payload shape.
Save this in .claude/settings.json at your project root (committed, so the whole team gets it) or ~/.claude/settings.json (just you, every project):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
I tested this exact pipeline rather than take the docs' word for it. Feeding it a simulated PostToolUse payload against a messy 4-line JavaScript file:
$ echo '{"tool_input":{"file_path":"messy.js"}}' \
| jq -r '.tool_input.file_path' | xargs npx prettier --write
messy.js 39ms
Single-quoted strings became double-quoted, inconsistent spacing collapsed to Prettier's defaults, and the whole thing took 39 milliseconds by Prettier's own reporting. Settings files are hot-reloaded, so this applies to the session you're already in - no restart needed.
Guarding it so a formatter-less repo doesn't break
The command above assumes every file Claude touches is something Prettier can parse. It isn't always true, and the failure is louder than a silent skip. I ran it against a plain .txt file to check:
$ echo '{"tool_input":{"file_path":"notes.txt"}}' \
| jq -r '.tool_input.file_path' | xargs npx prettier --write
[error] No parser could be inferred for file "notes.txt".
It exits non-zero and prints that error. PostToolUse hooks can't undo the edit either way, so this doesn't block anything - but it's noise you don't want on every README tweak or .env.example change. Add --ignore-unknown and the same run exits 0 without printing anything:
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write --ignore-unknown"
That single flag is the whole fix for Prettier. It doesn't exist for Biome - I tried --files-ignore-unknown=true against the same .txt file and it still printed a warning and exited 1. The workaround is a shell case on the extension instead of a flag:
"command": "f=$(jq -r '.tool_input.file_path'); case \"$f\" in *.js|*.jsx|*.ts|*.tsx|*.json|*.css) npx @biomejs/biome format --write \"$f\";; esac"
Prettier or Biome: what actually changes
Prettier
npx prettier --write <path>Measured format time
39ms self-reported, same 4-line test file
Default indent
2 spaces
Config file
.prettierrc*, prettier.config.*, or a package.json key
Unsupported file type
--ignore-unknown makes it a silent no-op (tested)
Biome
npx @biomejs/biome format --write <path>Measured format time
934µs self-reported - roughly 40x Prettier on the same file
Default indent
Tabs
Config file
biome.json or biome.jsonc
Unsupported file type
Exits 1 with a warning either way (tested) - guard by extension instead
Both formatters plug into the identical hook shape - same matcher, same tool_input.file_path field, same PostToolUse event. What changes is underneath. I ran both against the same test file so the comparison is measured, not lifted from either project's own benchmarks: Biome's self-reported format time was under a millisecond against Prettier's 39ms, which tracks with Biome being a single Rust binary versus Prettier's Node startup plus parse. The visible output differs too - Biome defaults to tabs, Prettier to 2 spaces - so switching an existing project from one to the other reformats every file it touches, not just the one Claude just edited.
If the project already has a .prettierrc or a biome.json, that config wins either way - these commands don't pass any formatting options of their own, so they inherit whatever the project already committed to.
The MultiEdit matcher you don't need anymore
What each matcher pattern actually catches
"matcher": "Edit|Write"Correct, currentThe official docs' own example. Catches every edit and every new file Claude creates.
"matcher": "Edit|Write|MultiEdit"Harmless, does nothingMultiEdit isn't a built-in tool name in the current hooks docs - this alternative never matches, so it's dead weight, not a bug.
"matcher": "Edit"Misses new filesSkips Write entirely, so every file Claude creates from scratch ships unformatted.
Checked against Claude Code's hooks docs and the /hooks browser, 2026-09-09.
A fair number of hook snippets floating around still write the matcher as "Edit|Write|MultiEdit", carried over from an older Claude Code tool set. I checked it against the current hooks documentation and the /hooks browser on Claude Code 2.1.267: MultiEdit doesn't appear as a built-in tool name anywhere in either. Multi-file edits go through Edit now, so the extra alternation in the matcher never has anything to match - it's not wrong, it just never fires. Edit|Write alone is the complete, current answer. The mistake worth actually avoiding is the opposite one: a matcher of bare Edit skips Write, so every file Claude creates from scratch - a new component, a new test file - ships unformatted until the next time something edits it.
Confirming the hook actually ran
Claude Code's own docs are explicit about this: when a hook succeeds, nothing shows up in the conversation. That's easy to mistake for "it isn't running." Three ways to actually check:
- Look at the file. The most direct test - ask Claude to add a line with single-quoted strings to a JS file, then open it. If quotes flipped to double and spacing normalized, the hook ran.
- Start with a debug log.
claude --debug-file /tmp/claude.log, thentail -f /tmp/claude.login another terminal - every hook that matched, its exit code, and its stdout/stderr shows up there. - Run
/hooksmid-session. It's a read-only browser as of the current docs (the old interactive editor is gone): every event, its matcher, and which settings file each hook came from.
When not to reach for this hook
This hook only fires after Claude's own Edit or Write tools - a Bash command that rewrites a file (a codegen script, a sed one-liner Claude ran itself) never triggers it, because PostToolUse matches on the tool that fired, not on what changed on disk. Claude Code's docs point to a FileChanged hook for that broader case: it watches specific filenames and fires on any change regardless of cause, at the cost of a matcher that lists files instead of tools.
It's also redundant, not harmful, alongside things you may already have: a pre-commit formatting hook, a CI check that fails on unformatted code, or an editor set to format-on-save. Running Prettier three times on the same file across three different triggers costs a little wall-clock time and nothing else - but if a large file is genuinely slow to format, that's where the overlap actually stings, and dropping this hook in favor of whichever of those you already trust is a reasonable call.
FAQ
What's the exact PostToolUse hook config to auto-format with Prettier in Claude Code?
Add this to .claude/settings.json: a PostToolUse hook with an Edit|Write matcher whose command is jq -r '.tool_input.file_path' | xargs npx prettier --write. That's the official docs' own example - it reads the edited file's path off the hook's JSON stdin with jq, then runs Prettier on exactly that file.
Does this hook work with Biome instead of Prettier?
Yes - swap the command for jq -r '.tool_input.file_path' | xargs npx @biomejs/biome format --write. Same matcher, same stdin field. Tested side by side on the same file, Biome's own self-reported format time was roughly 40x faster than Prettier's (934 microseconds versus 39 milliseconds), and it defaults to tabs where Prettier defaults to 2 spaces.
Why does my format-on-edit hook do nothing, or error, on some files?
Prettier errors with "No parser could be inferred" on a file type it doesn't recognize, like a .txt file - tested, it exits non-zero. Add --ignore-unknown to the Prettier command and that becomes a silent no-op instead. Biome doesn't have an equivalent flag - tested with --files-ignore-unknown=true and it still exits 1 with a warning - so guard it with a shell case statement on the file extension instead.
Do I still need MultiEdit in the matcher, like Edit|Write|MultiEdit?
No. Checked against Claude Code's current hooks documentation and the /hooks browser (as of Claude Code 2.1.267), MultiEdit does not appear anywhere as a built-in tool name - multi-file edits now go through the Edit tool. Edit|Write|MultiEdit still works, it's just that the MultiEdit alternative never matches anything. Edit|Write alone catches everything the current tool set can trigger.
Why doesn't anything show up in the Claude Code transcript when the hook runs?
That's expected - Claude Code's own docs confirm a successful hook shows nothing in the conversation. Confirm it ran by checking the file itself, or start the session with claude --debug-file /tmp/claude.log and tail the log, or run /hooks mid-session to see which hooks are registered and where each one is defined.
Will this hook fire when a Bash command rewrites a file instead of Claude's Edit tool?
No - PostToolUse only fires after a matched tool call, so a file rewritten by Bash (a build script, a codemod) never triggers it. Claude Code's own docs point to a FileChanged hook for that case: it watches specific filenames on disk and fires on any change, regardless of which tool caused it.
One hook, and the argument is over
Everything past pasting the JSON is Claude editing files and Prettier or Biome quietly cleaning up after it, so you stop typing "can you fix the quotes" as a separate prompt. It's one of the presets ClockedCode ships by default alongside a tuned CLAUDE.md and the rest of the curated set, next to the full 29-event hooks reference if you want to see what else runs on the same lifecycle, and the hook preset generator if you'd rather build the JSON by clicking than by hand.