Hooks: the part of the loop the model cannot skip
Claude Code
This page covers tools outside your selection. You can still read it. Find matching guides
Instructions are requests a model probably honours. A hook is code that runs at a lifecycle point whether the model would have chosen to or not.
Most of this site's Claude Code advice has a probabilistic caveat baked in: a CLAUDE.md rule is read every session and usually followed, a skill usually fires when relevant. Hooks are the one extension point with no "usually" in it. "Hooks are user-defined shell commands", and "Claude Code runs them at specific points in its lifecycle, which gives you deterministic control: certain actions always happen rather than relying on the LLM to choose to run them".
That sentence sorts your whole configuration. Anything that is a preference belongs in instructions. Anything that is a rule (must format, must never touch, must always notify) belongs where choosing is not involved.
What runs, and when
The documentation's own inventory: "format files after edits, block commands before they execute, send notifications when Claude needs input, inject context at session start". The lifecycle events do the targeting: PreToolUse runs before a tool call and can stop it, PostToolUse reacts after, and a matcher narrows the trigger, so a formatter fires on Edit|Write and not on every Bash call.
The interface is deliberately old-fashioned: "Hooks communicate with Claude Code through stdin, stdout, stderr, and exit codes". The hook reads the event as JSON on stdin, and the exit code is the verdict. Exit 0 lets the normal permission flow proceed; exit 2 blocks the action, with stderr fed back to Claude as the reason. A blocked tool call plus a one-line explanation is how the model learns your rule mid-session, which beats it silently retrying.
The canonical first hook is the docs' formatter: PostToolUse, matcher Edit|Write, run the project formatter on the touched file. Formatting disappears as a topic: from prompts, from review diffs, from CLAUDE.md.
For rules that need judgement instead of string-matching, there is a second tier: "For decisions that require judgment rather than deterministic rules", prompt-based and agent hooks put a model call at the lifecycle point. Useful, and it re-imports the probabilistic caveat the plain hook removed, so keep the hard floor (path blocks, command blocks) in ordinary code and let the model judge only what code cannot.
A hook is code you now trust completely
The power has a price the docs' examples imply: a hook is a shell command running with your permissions, on every matching event, written by whoever wrote the hook. Three consequences:
Vet hooks like you vet skills — stricter, in fact, since a skill is instructions the model interprets and a hook is code that simply executes. A hook from a plugin or a colleague's dotfiles is an installer you run hundreds of times a day.
Hooks widen the blast radius. A PreToolUse hook sees every tool input, which can include content that arrived by injection. Treat tool_input fields as untrusted strings: quote them, never eval them, and assume a hostile path or command will eventually flow through.
Failure is loud and yours. "Since hooks run in parallel, the order is non-deterministic" — so two hooks editing the same file is a race you built. And a hook that hangs or exits wrong on every event degrades the whole session in a way that looks like "Claude Code is broken" until you think to check /hooks.
Where hooks fit in the toolbox
The division of labour, one line each: CLAUDE.md states facts and preferences the model should know. Skills package procedures the model applies when relevant. Permissions define what the model may ask to do. Hooks enforce what happens regardless. The common misconfiguration is one level up from where it belongs: a "never touch .env" line in CLAUDE.md is a request; the same rule as a PreToolUse block is a fact about the session.
What goes wrong
The rule that is merely written down. The instruction held for forty sessions, and the one where it slipped is the one that mattered. If a violation is unacceptable, "asked nicely" was the wrong mechanism.
The hook that trusts its input. An unquoted $FILE_PATH in a shell hook is an injection point fed by whatever the model read today.
The everything-hook. No matcher, heavy work, every event — each model turn now waits on your script, and the session crawls.
Model-judged floors. The agent hook asked to "block anything dangerous" inherits every calibration problem hooks exist to escape. Judgement above, hard rules below.
Nobody knows the hook exists. Blocked actions with no documented reason read as flakiness to a teammate. The stderr message is documentation; write it for the person who did not install the hook.
How to check it worked
Break-test it, the same way you test any gate: ask Claude directly to do the forbidden thing — edit the protected file, run the banned command — and watch the block land with a readable reason. Then check the cost side: a session's worth of normal work with time on the hook script, because a gate that adds seconds to every edit gets deleted by its own victims within a month.
Sources
- Automate workflows with hooks — Claude Code documentation Tier 1 2026-09-04
Something wrong with this page?
Say what you expected and what you got. That is usually the shortest route to a correction, and it goes on the public issue tracker so the fix is visible.