Key Takeaway
Claude Code hooks are shell commands that run automatically at fixed points in the agent's lifecycle, such as before a tool call or when a task finishes. Because they execute as deterministic code rather than model suggestions, they enforce guarantees the model can forget: formatting every edit, blocking writes to protected paths, and running tests as a quality gate.
Prompting an agent to always run the formatter works until the one time it forgets, and with a coding agent that one time lands in your diff. Instructions in a prompt are suggestions the model may or may not follow. Hooks are the opposite: shell commands the runtime executes for you, every time, regardless of what the model decides.
This guide explains what Claude Code hooks are, the lifecycle events you can attach to, the guardrail patterns worth adding first, and how a hook blocks a dangerous action before it runs. The mechanics are specific to Claude Code, but the idea of deterministic gates around a non-deterministic agent is universal.
What a hook actually is
A hook is a shell command that Claude Code runs automatically when a specific event fires, configured in your settings.json rather than in a prompt. The model never decides whether a hook runs; the runtime does. That single property is what makes hooks useful, because it converts a hope into a guarantee: not please lint this, but this was linted.
Hooks receive structured JSON about the event on standard input, so a hook script can inspect exactly which tool is about to run and with what arguments. Three things set them apart from other steering mechanisms:
- They are deterministic. The same event always triggers the same command, so behavior does not drift across a long session the way a prompt instruction can.
- They run your own code, so any check you can express as a shell script or a program can become a gate, from a formatter to a custom policy engine.
- They fire on lifecycle events, not on the model's judgment, which is why they are the right tool when you need something to happen without fail.
The lifecycle events you can attach to
Hooks are grouped by event in settings.json. The most used are PreToolUse, which runs before a tool call and can block it; PostToolUse, which runs after a tool succeeds and is ideal for formatting or linting the file just changed; and Stop, which runs when Claude finishes responding and makes a natural quality gate. There are also events for prompt submission, notifications, and session start.
A common starter setup formats every edited file and screens each Bash command through a small guard script:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "npx prettier --write \"$CLAUDE_FILE_PATHS\"" }
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": ".claude/guard-bash.sh" }
]
}
]
}
}Match hooks narrowly. A PostToolUse matcher of Edit or Write should format only the files that actually changed, using the paths Claude Code passes in the event payload, rather than reformatting the whole repository on every edit and burying your diff in noise.
Guardrail patterns worth adding first
You do not need many hooks to feel the benefit. A few well-chosen ones cover the mistakes agents make most often.
- Auto-format on write: a PostToolUse hook that runs Prettier, gofmt, or Black on the changed file, so style is never part of the model's job.
- Protected paths: a PreToolUse hook that blocks edits to lockfiles, generated code, or CI config, refusing the write instead of trusting the model to leave them alone.
- Test gate on Stop: a Stop hook that runs your fast test command and reports failures, giving the agent an automatic signal to keep working rather than declaring victory early.
- Audit log: a hook that appends every Bash command to a file, so you can review after the fact exactly what the agent ran.
How a hook blocks a dangerous action
PreToolUse hooks are the enforcement point. Before Claude Code runs a matched tool, it runs your hook and reads the result. If the hook exits with a blocking status, the tool call is denied and the reason is fed back to the model, which then has to choose a different path. This is how you stop an action deterministically instead of asking the model not to do it.
Because the hook sees the full tool input as JSON, the decision can be precise: allow a Bash command that only reads, reject one that pipes a remote script into a shell, and let everything else through. The model cannot talk its way past a hook, which is exactly why security-relevant rules belong here and not in a CLAUDE.md instruction.
A hook runs with your full user permissions and no confirmation prompt, so a careless hook command is its own risk. Treat every hook script as production code: quote your variables, avoid blindly interpolating tool input into a shell, and test the script by hand before wiring it to an event that fires on every tool call.
Keep hooks fast and predictable
Hooks sit on the critical path of the agent loop, so a slow or flaky hook makes the whole session feel slow. A few habits keep them out of the way.
- Keep them fast. A PostToolUse hook that runs on every edit should finish in well under a second, so target the changed file, not a full-project build.
- Fail clearly. When a hook blocks an action, print a short reason to standard error so the model gets a usable explanation instead of an opaque failure.
- Version them with the repo. Commit project hooks in your settings so the whole team gets the same guardrails, and keep personal ones in your user settings.
Hooks are how you stop repeating the same correction to an agent. Anything you find yourself asking for every session, such as format this, do not touch that, or run the tests before you stop, is a candidate for a hook, and once it is one it happens whether the model remembers or not. Start with a formatter and a test gate, then add guards as you learn where your agent slips.

