Skills raise the odds that Claude does what you want. Hooks remove the odds and make it deterministic. A hook is your own script that Claude Code runs automatically at a fixed point in its lifecycle: before it uses a tool, after it runs a command, when you submit a prompt, or before it declares the work done. Claude does not get a vote. The script runs, and its exit code can block the action.
That difference is the whole reason hooks exist. A CLAUDE.md rule is a suggestion the model can ignore. A hook is code. It fires whether the model cooperates or not.
The four events I actually use
Claude Code fires hooks at named points in its run. These are the four I have wired in ~/.claude/settings.json on this machine, matched to specific tools:
| Event | Fires | I use it to |
|---|---|---|
UserPromptSubmit | Every prompt I send, before Claude reads it | Inject fresh project context |
PreToolUse | Before Claude runs a tool (Bash, Edit, Write) | Block dangerous commands, warn on risky edits |
PostToolUse | After a tool finishes | React to what just happened, like a commit |
Stop | When Claude tries to end the turn | Force verification before “done” |
Each entry has a matcher (which tool it watches) and a list of scripts. A PreToolUse hook matched to Bash runs before every shell command. Matched to Edit|Write, it runs before every file change.
PreToolUse: the accident floor
The hook I lean on hardest is a PreToolUse matcher on Bash called auto-guardrail.sh. It reads the command Claude is about to run and, if it matches a catastrophe pattern, exits with code 2, the one exit code that blocks the tool call before it happens. Other non-zero codes just surface a warning and let the command through. Only exit 2 is a wall.
I hit it for real while writing this. I piped a curl straight into a Python interpreter to parse a page, and the hook denied it: remote code execution is on its blocklist. The command never ran. That is the point of PreToolUse. It is not advice the model weighs. It is a wall the model runs into, and the wall wins.
Alongside it I run check-dependents.sh and require-trace.sh on Edit|Write, so before Claude changes a file I get told what depends on that file. The edit waits for that context to load.
Stop: no declaring victory early
The Stop event is the one that changed my results the most. When Claude decides it is finished, three scripts fire first, including verify-before-done.sh. Its whole job is to refuse the “all done” until the thing was actually checked.
Left alone, an AI will tell you a task is complete because the code looks right. Looks right is not works right. A Stop hook is where you enforce the difference: run the test, curl the endpoint, prove it before the turn ends.
UserPromptSubmit: context on every turn
UserPromptSubmit fires before Claude even reads your message. Mine runs proactive-recon.sh, which scans the project and hands Claude a fresh map: complexity hotspots, config drift, recently changed files. The agent starts every prompt with current ground truth instead of a stale memory of the repo. I did not type any of it. The hook injected it.
Config: what a hook looks like
Hooks live in settings.json as an event, a tool matcher, and the scripts to run:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "/Users/you/.claude/scripts/auto-guardrail.sh", "timeout": 5 }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "/Users/you/.claude/scripts/verify-before-done.sh", "timeout": 10 }
]
}
]
}
}
Each hook is a command object, not a bare path, and a PreToolUse entry takes a matcher while a Stop entry does not. The script reads the tool input on stdin and signals with its exit code. Exit 0 lets the action through. Exit 2 blocks it and hands your message back to Claude as feedback. That two-way channel is why a hook can steer the agent, not just watch it.
Claude Code hooks are not git hooks
One thing that trips people up: these are not the same as a git pre-commit hook. A Claude Code hook fires around the agent’s own tool calls, inside your session. A git pre-commit hook fires when anyone runs git commit, agent or human, at the version-control layer.
They stack. I use both. The pre-commit hook that blocks bad AI code is the last gate before code lands in the repo, and it caught 104 policy violations in a codebase I thought was clean. The Claude Code PreToolUse hook is earlier and broader: it governs what the agent does at all, not just what it commits. Reach for the git hook to protect the repo. Reach for the Claude Code hook to govern the agent.
The rule
If you need the agent to reliably do or not do something, a rule in CLAUDE.md will not get you there. A hook will. Put guidance in skills and CLAUDE.md. Put anything load-bearing behind a hook, because a hook is code and the model cannot talk its way past it.
Start with two: a PreToolUse matcher on Bash that blocks the commands you never want run, and a Stop hook that refuses “done” until the work is verified. Those two alone change how much you can trust an agent working while you are not watching.
For the full contract of every event and its exit codes, see Claude Code’s hooks documentation.
Related: