How to Auto-Format Code After Every Edit With Claude Code Hooks (Step-by-Step)
Claude Code writes good code, but it doesn't always write it in your style. Two-space indents in one file, four in the next, a trailing comma here and not there. You can ask it to run the formatter every time, and it'll forget by the third edit. The fix is a Claude Code hook: a small rule that runs your formatter automatically after every file edit, with no reminder needed. This tutorial walks you through it from an empty folder to a working setup.
Quick answer: Add a PostToolUse hook with the matcher Edit|Write to .claude/settings.json in your project. Its command reads the edited file path from the hook's JSON input with jq -r '.tool_input.file_path' and passes it to your formatter, for example xargs npx prettier --write. It then runs after every edit Claude makes.
What You'll Build
By the end you'll have:
- ✓ A one-line hook that runs Prettier on every file Claude edits or creates
- ✓ A small script version that picks the right formatter per language (Prettier, Ruff, gofmt)
- ✓ A settings file you can commit, so everyone on the project gets the same behavior
When I set this up for a client's codebase, it's usually the very first hook I add. It's harmless, it saves review time, and it shows the team what hooks can do before we try anything that blocks or changes Claude's behavior.
Prerequisites
- Claude Code installed and working in a terminal inside your project folder.
- jq, the command-line JSON tool. Anthropic's hook examples use it. Install it with
brew install jqon macOS orapt-get install jqon Debian and Ubuntu. - A formatter for your language. This guide uses Prettier for JavaScript and TypeScript, plus Ruff and gofmt in the multi-language version.
- Five minutes and a test file you don't mind Claude rewriting.
How Hooks Work (in 60 Seconds)
A hook is a shell command that Claude Code runs at a specific moment in its lifecycle. Each hook has three parts:
- An event: when it fires.
PostToolUsefires right after Claude uses a tool, such as editing a file. - A matcher: which tools it applies to.
Edit|Writemeans "only the file-editing tools". - A handler: what runs. Here it's a
commandthat receives details about the tool call as JSON on standard input.
That JSON includes the tool name and the tool's input. For file edits, the input holds file_path, which is exactly what your formatter needs.
Step 1: Pick Where the Hook Lives
Hooks live in Claude Code settings files. Which one you pick decides who gets the hook:
| File | Applies to | Shared? |
|---|---|---|
~/.claude/settings.json | All your projects | No, just your machine |
.claude/settings.json | This project | Yes, commit it to the repo |
.claude/settings.local.json | This project | No, kept out of git |
For formatting, I use the project file. The formatter config already lives in the repo, so the hook should too. Create the folder if it's missing:
mkdir -p .claude
Step 2: Install Prettier in the Project
Install Prettier as a dev dependency so the hook uses the same version as everyone else:
npm install --save-dev --save-exact prettier
If you already have a .prettierrc, Prettier picks it up automatically. If you don't, it uses its defaults, which is fine for a first test.
Step 3: Add the One-Line Hook
Open .claude/settings.json and add this. If the file already has other settings, merge the hooks key in alongside them.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
Here's what each piece does:
PostToolUseruns the hook after the edit has happened, so Prettier sees the final file.Edit|Writelimits it to file edits and new files. Bash commands and file reads don't trigger it.jq -r '.tool_input.file_path'pulls the edited file's path out of the JSON that Claude Code sends to the hook.xargs npx prettier --writehands that path to Prettier, which rewrites the file in place.
This is the same pattern Anthropic uses in its own hooks guide, so it's a safe starting point.
Step 4: Check That Claude Code Sees It
Start Claude Code in the project and type /hooks. This opens the hooks browser, where you should see your entry listed under PostToolUse. If it isn't there, check the JSON for a missing comma or bracket, then start a fresh session.
Step 5: Test It With a Messy File
Ask Claude for something you know will need formatting:
Create src/demo.js with a function that adds two numbers.
Use single quotes and no semicolons.
Open src/demo.js after Claude finishes. If your Prettier config uses double quotes and semicolons (the default), you'll see them, even though Claude wrote the opposite. A successful hook shows nothing in the conversation. It just quietly does its job.
Step 6: Upgrade to a Multi-Language Script
The one-liner sends every file to Prettier, including Python or Go files Prettier can't format. On a mixed codebase, a short script is cleaner. Create .claude/hooks/format.sh:
#!/usr/bin/env bash
# Read the hook input and pull out the edited file path
FILE_PATH=$(jq -r '.tool_input.file_path // empty')
# Nothing to format? Exit quietly.
[ -z "$FILE_PATH" ] && exit 0
case "$FILE_PATH" in
*.js|*.jsx|*.ts|*.tsx|*.json|*.css|*.md)
npx prettier --write "$FILE_PATH" ;;
*.py)
ruff format "$FILE_PATH" ;;
*.go)
gofmt -w "$FILE_PATH" ;;
esac
exit 0
Make it executable:
chmod +x .claude/hooks/format.sh
Then point the hook at the script instead of the one-liner. $CLAUDE_PROJECT_DIR is set by Claude Code to your project root, so the path works no matter which folder Claude is in:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format.sh"
}
]
}
]
}
}
The // empty part of the jq filter returns nothing instead of the word null when there's no file path, so the script exits cleanly instead of trying to format a file called "null". Remove any language line you don't use.
Step 7: Commit It for the Team
Add .claude/settings.json and .claude/hooks/format.sh to git. Anyone who opens the project in Claude Code now gets formatted output without setting anything up. If you'd rather keep the hook to yourself, put the same JSON in .claude/settings.local.json instead.
Troubleshooting
Nothing happens after an edit
Run /hooks and confirm the entry is there. Then check that jq is installed (jq --version) and that your formatter runs on its own in the same terminal.
"hook error" shows up in the transcript
Claude Code reports a non-blocking error when a hook exits with a code other than 0 or 2. Usually the formatter couldn't parse the file or wasn't found. Start Claude Code with claude --debug to see the hook's output, or test the script by hand:
echo '{"tool_input":{"file_path":"src/demo.js"}}' | .claude/hooks/format.sh; echo $?
A 0 at the end means the script succeeded.
The script runs but says "Permission denied"
You skipped chmod +x. Run it again on .claude/hooks/format.sh.
Files changed by Bash commands aren't formatted
That's expected. The Edit|Write matcher only covers Claude's file tools. Anthropic's guide points to a separate FileChanged hook event if you need to catch files changed some other way.
The formatter is slow on big files
Command hooks have a default timeout of 600 seconds, so a slow formatter won't get cut off, but you'll feel the wait after each edit. Format only the one file (as both versions above do), never the whole project.
Where to Go Next
Once auto-formatting works, the same pattern handles lint checks, test runs on changed files, or blocking edits to files like .env. If you want Claude to follow a process rather than run a command, a skill is the better tool: see my guide on building a Claude Skill step by step. For jobs that should run on a timer instead of after an edit, read how to set up a scheduled task in Claude Cowork. And if you're comparing AI tools for your dev pipeline, my roundup of AI DevOps tools covers the wider picture.
Frequently Asked Questions
What is a PostToolUse hook in Claude Code?
It's a hook that runs right after Claude finishes using a tool, such as editing a file or running a command. Because the edit has already happened, it's the right event for formatting, linting or logging.
Where do I put Claude Code hooks?
In a settings file. Use .claude/settings.json to share them with your team through git, .claude/settings.local.json for the current project only, or ~/.claude/settings.json to apply them to all your projects.
Can I use Black or ESLint instead of Prettier?
Yes. The hook only passes the file path to a command. Swap npx prettier --write for any tool that accepts a file path, such as black for Python or npx eslint --fix for JavaScript.
Do hooks slow Claude Code down?
Only by however long your formatter takes on one file, which is usually well under a second. Keep the command focused on the edited file and you won't notice it.
How do I turn a hook off temporarily?
Remove or rename its entry in the settings file. To switch off every hook at once, Anthropic documents a disableAllHooks setting you can set to true.
Want this built for your business?
I'm Basant, an AI automation consultant. Book a call: https://topmate.io/basant_narayan_singh
See more of my work at singhbasant.com.
Join the conversation