Write “always run the formatter after editing” in a project file and it will happen most of the time. Most of the time is a wonderful property for a colleague and a terrible one for a build step, and the gap shows up as review comments about whitespace, which is the least interesting way to spend a review.
Hooks close that gap by moving the rule out of the conversation entirely.
| Advice | Guarantee | |
|---|---|---|
| what it is | “always format after an edit” | a PostToolUse hook |
| lives in | CLAUDE.md | settings.json |
| runs when | remembered | every time |
| costs | tokens, every turn | nothing |
| fails | silently | loudly |
Where a hook gets a say
A hook is a shell command Claude Code runs at a fixed point in its own lifecycle. Your script, your machine, and its output comes back to the agent as feedback, which is how a hook refuses something and explains itself in the same breath.
- 01 You ask Something you want done.
- 02 A tool is chosen PreToolUse runs here. Your script sees the call first and can refuse it, with a reason.
- 03 The tool runs Bash, Edit, Write, and the rest. PostToolUse runs after it: formatters, codegen, the lint you keep forgetting.
- 04 The result is read Claude reads what came back and carries on.
- 05 It tries to finish Stop runs here, and can send it back to work.
They are configured in settings.json and matched against the tool being called, so a hook can apply to every edit, or only to Bash, or only to a subset you match with a pattern.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": ".claude/hooks/format-changed.sh" }
]
}
]
}
} The hook receives the tool call as JSON on standard input, so the script knows exactly which file was written and can format only that one instead of the whole tree. Keep the script in the repo next to the config; a hook that shells out to a one liner is a hook nobody can debug in six months.
Three hooks worth having
- Format on write. The classic, and the one that deletes a whole category of review comment. PostToolUse on
Edit|Write. - Refuse a command class. A PreToolUse hook on Bash that reads the command and exits non-zero for the ones you never want run unattended. This is a real guarantee, unlike a line in CLAUDE.md asking politely.
- Rewrite a command into the one you meant. If every
gitcall in your project should really go through a wrapper, a hook can do that substitution every single time, with no cooperation from the model and no tokens spent explaining it.
That third one is worth dwelling on, because it is the pattern people miss. Anything you find yourself correcting in the transcript, over and over, is a candidate for silent substitution. You are not teaching the agent a preference; you are removing the choice.
Which settings file
There are three, and the difference is a team question rather than a technical one.
~/.claude/settings.jsonfollows you into every project. Your habits live here..claude/settings.jsonis committed. Team conventions live here, and everyone gets them on clone..claude/settings.local.jsonis gitignored. Your personal permission allowlist for this repo lives here, because nobody else wants it.
A hook is code, with no model in the loop
This is the part to take seriously. A hook runs on every matching event, immediately, with no judgement applied. A PostToolUse hook with a bad path will happily corrupt every file the agent touches, quickly, and the agent will keep working on the wreckage because as far as it knows the write succeeded.
- Run the command by hand before you wire it to an event. Every time.
- Make it a no-op when it does not apply, rather than an error, or you will spend the session reading hook failures.
- Keep it fast. It runs on every matching tool call, and a two second hook on a busy session is a tax you will feel.
- Log what it did somewhere you can read afterwards.
The Stop hook is the interesting one
PreToolUse and PostToolUse are about individual actions. The Stop hook is about the ending: it runs when the agent tries to finish, and it can send it back to work. That is a different kind of power, because it is the only one that can enforce a definition of done rather than a definition of behaviour.
It is also, not coincidentally, the mechanism behind the goal command in the last post of this series. Once you have seen the hook, that feature stops looking like magic and starts looking like a gate you could have built yourself.