Author: abavyka
A hook is a command Claude Code runs on its own at the right moment, not because the model decided to.
What it is
You write in the settings: on this event, run this command. An event is a moment in Claude Code’s work: before a tool call, after it, when Claude is waiting on you, when it’s done answering, session start, compaction, over thirty of them in total.
The difference from CLAUDE.md is that CLAUDE.md is advice, and the model might not follow it. A hook fires every time. That’s why I wrote earlier “the critical stuff goes in hooks”.
Most often a hook is a shell command ("type": "command"). There’s also:
http— a POST with the event data to your URLmcp_tool— a tool call on a connected MCP serverprompt— one question to a model that decides “yes/no”, for when a rule can’t describe itagent— a subagent that can read files and run commands on its own before deciding (experimental)
How it works
Config in three levels: event → matcher (which tool, e.g. Bash or Edit|Write) → commands.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
]
}
]
}
}This is an example from the docs: after every file edit, Prettier formats it.
As input (stdin) the hook gets JSON: tool_name, tool_input (for Bash there’s command in it, for Edit — file_path), cwd, session_id.
The hook answers with an exit code:
0— all good2— block the action, stderr goes to Claude as the explanation of why not- anything else — a hook error, but the action goes ahead
So exit 1 blocks nothing. Want a ban — only exit 2.
Or the hook writes JSON to stdout. For PreToolUse that’s:
permissionDecision—allow,deny,askordeferupdatedInput— swap the tool’s arguments before it runsadditionalContext— toss Claude some text next to the result
Where they live
~/.claude/settings.json— all your projects.claude/settings.json— the project, committed, the whole team gets the same hooks.claude/settings.local.json— the project, just you- also in plugins (
hooks/hooks.json), in the frontmatter of skills and subagents, and in managed settings from your admin
Hooks from all levels stack up instead of overwriting each other. All the ones that match an event run in parallel.
/hooks— see all hooks by event (view only)"disableAllHooks": true— turn them all off. You can’t turn off one while keeping it in the config
Mine
guard-main— in this wiki’s repo,.claude/settings.json,PreToolUseonBash. Doesn’t let Claude commit or push tomainor merge a PR. It’s a workaround: on free GitHub a private repo has no branch protectionpeon-ping—Notification,PermissionRequest,PostToolUseFailure. The Warcraft III peon’s voice when a session needs my attention. A useful gimmickcc-status— iTerm2 installs it itself (Claude Code Integration). Shows the session status in the tab: working, waiting, idle. I barely use it, basically it’s just UIrtk—PreToolUseonBash, rewrites the command viaupdatedInput:git status→rtk git status, and Claude sees compressed output
guard-main has a wrapper in sh that blocks commit and push via exit 2 when there’s no Node 24. Without it the hook on Node 20 would crash with an error, and an error, as above, blocks nothing.
What else you can do
Examples from the docs and stuff I mentioned before:
- Prettier after every
Edit|Write— config above - A ban on editing
.env,package-lock.jsonand everything in.git/—PreToolUseonEdit|Write, the script looks atfile_pathand doesexit 2. Claude gets the explanation and looks for another way - Bring back the important stuff after compaction —
SessionStartwith matchercompact, everything the command prints to stdout lands in the context. E.g.git log --oneline -5or “we use pnpm, not npm” - Typecheck after every edit, not by asking —
PostToolUseonEdit|Writewithnpx tsc --noEmit 1>&2 || exit 2. OnPostToolUseexit 2rolls nothing back (the file is already changed), but the tsc errors go to Claude, and it fixes them - Don’t let Claude go until the tests are green — a
Stophook of typeagent: a subagent runs the tests, and if they fail, Claude keeps working - Output filter — the hook cuts everything but
ERRORout of a 10,000-line log before Claude sees it. Hundreds of tokens instead of tens of thousands.rtkis exactly about this
Gotchas
- A hook isn’t a lock. A hook timeout or error doesn’t block the action unless
onFailure: "block"is set (since 2.1.295). For a hard allow/deny there are permissions - On the flip side, a
denyfrom a hook blocks even inbypassPermissions - Two hooks with
updatedInputon the same tool — the one that finished last wins, i.e. a random one - Files you added yourself via
@go into the prompt without a tool call, soPreToolUseonReaddoesn’t see them - A
Stophook that blocks 8 times in a row without a single tool in between gets cut off by Claude Code, and it ends the answer - A hook runs any command with your permissions. Hooks from someone else’s repo’s
.claude/settings.jsonwork inclaude -peven without the trust dialog. Cloned it — take a look at what’s in the hooks
Read more
- Hooks in Claude Code
- Automating checks with hooks
- CLAUDE.md
- Hallucinations
- Tokens and limits
- Getting started and examples: Automate actions with hooks
- All events, exit codes and JSON: Hooks reference
- What runs before the trust dialog: Configure permissions
- Output filtering with a hook: Manage costs effectively
- peon-ping
- iTerm2 Claude Code Integration
- rtk