Claude Code Hooks: Deterministic Agent Guardrails

Claude Code hooks are shell commands that the runtime executes automatically at defined points in the agent's lifecycle, such as before or after a tool call, or when a task finishes. They are configured in settings.json rather than in a prompt, so they run every time regardless of what the model decides. This makes them the mechanism for guarantees the model might otherwise forget.
A prompt instruction is a suggestion the model may or may not follow, and its influence drifts over a long session. A hook is deterministic code the runtime runs unconditionally, so a formatting or safety rule enforced by a hook happens on every matching event. Use prompts for guidance and hooks for guarantees.
Yes. A PreToolUse hook runs before a matched tool executes, and if it exits with a blocking status the tool call is denied and the reason is fed back to the model. Because the hook receives the full tool input as JSON, it can allow safe commands and reject dangerous ones precisely, which is why security rules belong in hooks.
Start with a PostToolUse hook that formats the file just edited, a PreToolUse hook that blocks writes to lockfiles or generated code, and a Stop hook that runs your fast test command as a quality gate. These three cover the most common agent mistakes and take only a few lines of settings.json each.
Hooks run with your full user permissions and no confirmation prompt, so a careless hook script is a real risk. Treat hook scripts as production code: quote variables, avoid interpolating raw tool input into a shell, and test each script by hand before wiring it to an event that fires often. Committing hooks to the repo also lets your team review them.

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.
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:
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.
You do not need many hooks to feel the benefit. A few well-chosen ones cover the mistakes agents make most often.
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.
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.
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.