What are Claude Code hooks?
Claude Code hooks are handlers you register for fixed points in the lifecycle of a Claude Code session. Anthropic's hooks reference defines them as "user-defined shell commands, HTTP endpoints, MCP tool calls, LLM prompts, or subagents that execute automatically at specific points in Claude Code's lifecycle". The points are events such as the start of a session, the moment before a tool call, the moment after it, and the end of the agent's turn.
What sets a hook apart from an instruction is who decides. A line in AGENTS.md or a skill asks the model to behave a certain way, and the model may or may not follow it. A hook runs every time its event fires, whatever the model intended, and a hook on the right event can stop the action outright.
In plain words
Think of the turnstile at an office entrance. The house rules on the wall ask visitors to sign in, and most of them do. The turnstile does not ask: without a badge, the gate stays shut, whoever you are and however good your reason. Instructions for an AI agent are the house rules. A hook is the turnstile.
How hooks work
A hook has three parts: the event it listens to, a matcher that narrows which occurrences of the event it cares about, and the handler that runs.
- Events. The reference lists more than thirty of them, from
SessionStartandUserPromptSubmittoPreToolUse,PostToolUseandStop.PreToolUseis the place to start when you want to enforce a rule, because it runs before a tool call and can block it. - Matchers. For tool events the matcher is tested against the tool name:
Bash,Edit|Write, or a regular expression such asmcp__.*for every MCP tool. - Handlers. The reference describes five types: a shell command, an HTTP endpoint, an MCP tool call, a single-turn prompt to a Claude model, and an experimental agent hook that can read files before it decides. Every hook our own agents run is a command.
A command hook receives the event as JSON on standard input and answers with its exit code. Exit 0 lets the action go ahead, unless JSON the hook printed says otherwise. Exit 2 is a blocking error: on events that can block, Claude Code stops the action, and on PreToolUse that means the tool call does not run. Any other exit code is a non-blocking error, and the action goes ahead, unless the hook is set to block on failure.
Hooks live in settings files, in your user settings (~/.claude/settings.json), in a project's .claude/settings.json that you commit with the code, in a local untracked file, or in organisation-wide managed settings. Plugins can bundle them in hooks/hooks.json, and skills and subagents can declare hooks in their own frontmatter. A minimal project hook that checks every shell command looks like this:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "bash \"${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-push.sh\"", "timeout": 5 }
]
}
]
}
}
#!/usr/bin/env bash
# Reads the tool call from stdin and refuses a push that names main.
cmd="$(jq -r '.tool_input.command // empty')"
if grep -Eq 'git[[:space:]]+push.*[[:space:]](main|master)([[:space:]]|$)' <<< "$cmd"; then
echo "Pushing to main is not allowed here. Open a pull request instead." >&2
exit 2
fi
exit 0
Why they matter
- Rules that hold when nobody is watching. An instruction is a request. A
PreToolUsehook that exits 2 is a decision the model cannot talk its way around. That difference matters most in long unattended runs, where nobody reads each step. - Routine work stops depending on memory. A hook can format a file after every edit, run a linter, add context when a session starts or log each tool call. Anthropic's hooks guide walks through several of these. A hook does the job every time, not only when the model remembers.
- Policy ends up in one place. A hook in a project's
.claude/settings.jsonor in a plugin travels with the code. The whole team, and every agent working in the repository, gets the same rule from the same file.
We use them this way on our own headless agents. A plugin of hooks runs before every shell command. On the agents that work on their own code, one of them refuses a push to main and any pull request merge the model attempts. That code reaches main only through a pull request a person approved. It fails closed: when it cannot read the agent's configuration, it applies the strictest rule. A second hook checks the text of every GitHub comment an agent is about to post, and allows an @mention only for members of the organisation and only within working hours.
What to watch out for
- By default, only exit 2 blocks. Exit 1, the usual Unix failure code, is a non-blocking error, and the action goes ahead. The reference says it plainly: if a hook is meant to enforce a policy, use
exit 2. A guard with a wrong path or a crashing script lets everything through. Since Claude Code v2.1.295 you can set"onFailure": "block"on the hook to block the action instead. That also covers a script that never starts, which anexit 2inside the script cannot. - A timeout lets the call through. The default timeout for a command hook is 600 seconds, and on
PreToolUsea hook that times out does not block the tool call unless it hasonFailureset to block. Keep guards fast and set the timeout explicitly. Ours run with limits of 5 to 20 seconds. - Hooks run with your permissions. Anthropic's own warning is that command hooks "execute shell commands with your full user permissions". In an interactive session, Claude Code holds hooks back until you trust the folder. In a
claude -por SDK session it treats the folder as trusted, so hooks committed in someone else's repository run without a prompt. Review a repository's.claude/settings before you script Claude Code over it. - A hook sees the call, not its effect. A guard reads the command text and the context it is given, nothing else. Our push guard decides about a bare
git pushby the branch checked out in the directory where the shell runs, so our agents always name the remote and the branch. A rule written against text has to know which text it will see. It also matches literally. The AI agent sandbox entry describes a run our guard refused because a forbidden command appeared only as quoted text. - A hook is not a security boundary. The header of our comment guard puts it in one line: "A guard over the model's own calls, not a security boundary." Scripts that run outside the model's tool calls never pass through it. To bound what a process can reach, use a sandbox and scoped credentials. Hooks are for keeping the agent on the rails you agreed on, alongside AI guardrails and human-in-the-loop review.
Related articles:
- What is AGENTS.md? – The instruction file a hook can enforce instead of merely stating.
- What are AI agent skills? – On-demand procedures for an agent, which can declare their own hooks.
- What is an AI agent sandbox? – The runtime boundary that limits what a command can reach, where a hook only decides whether it runs.
- What are AI guardrails? – The wider family of controls that hooks belong to.
- What is prompt injection? – The attack that turns an agent's instructions against you, and why a rule outside the model helps.
- Customizing GitHub Copilot when MCP is off the table – GitHub Copilot's own hooks, configured in
.github/hooks/.
Want to stay one step ahead?
Don't miss our best insights. No spam, just practical analyses, invitations to exclusive events, and podcast summaries delivered straight to your inbox.
