Claude Code Hooks Explained: Deterministic Guards for the Agent Loop

June 23, 2026 · How Claude Actually Works (part 9)

▶ Watch on YouTube & subscribe to The Stack Underflow

System prompts are great — until a persuasive user talks the model out of following them. A prompt that says “never refund more than $500” is a guideline written in sand. Hooks are the same rule written in concrete. They sit on the execution path between the model’s decision and actual tool invocation, and they are evaluated by your code, not the model’s judgment.

This tutorial covers what hooks are, the lifecycle events they fire on, the four handler types available, the exit-code contract that drives allow/block decisions, and the one failure mode you absolutely must handle before shipping anything to production.

The one-sentence version: Claude Code hooks are code-level lifecycle callbacks that intercept the agent loop at specific moments and can block, allow, or augment tool calls in a way the model cannot reason its way around.

Why prompts alone are not enough

Consider an agent with this system prompt: “Never refund more than $500.” A determined user can rephrase their request, add context that reframes the situation, or simply ask again. Language models are, by design, responsive to language. Given a compelling enough argument, the model can emit process_refund(amount=750) and the ledger takes the hit.

Add a pre-tool-use hook on the same path and the situation changes completely. The hook intercepts the call before execution. It doesn’t care how eloquent the user was. It reads amount, compares it to 500, and returns a structured block with a reason the model can actually use:

{
  "decision": "block",
  "reason": "policy_cap",
  "retriable": false
}

The model receives this, understands the rule, and proposes a compliant $500 refund. Code held the line; the prompt never could have.

The three-layer defense model

Hooks are the third and final layer in a defense-in-depth stack:

Layer 1 — System prompt guidance       (soft, dashed line)
Layer 2 — Tool description constraints (medium strength)
Layer 3 — Hook enforcement             (hard backstop)

The $750 refund attempt passes through layers one and two and bounces off layer three. Each layer catches what the one above it missed. None of them are redundant — prompts cover the 99% of normal cases efficiently; hooks exist for the 1% where the cost of failure is too high to leave to probability.

Lifecycle events: when hooks fire

Hook events are not a flat list of interchangeable callbacks. They fire at three distinct cadences:

CadenceWhenTypical use
Per sessionOnce on session start; once on stopInject project context, teardown audit records
Per turnOnce per user prompt (before processing)Block a prompt class before any tool runs
Per tool callEvery time a tool is about to run or has finishedEnforce policies, audit individual operations

The four hooks that cover roughly 80% of real-world use cases, according to Anthropic’s documentation:

  • pre_tool_use — fires before a tool executes; can block it
  • post_tool_use — fires after execution; useful for audit trails
  • session_start — fires once when a session opens
  • session_stop — fires once when a session closes

The other 11+ event types (sub-agent events for multi-agent observability, pre_compact for context management policy, permission_request for granular control) exist for more specific scenarios. Start with the four above and reach for the rest when a problem actually requires them.

Four handler types

A hook is not required to be a shell script, though that is what ~90% of real-world hooks are. The four handler types are:

  1. Command — an executable or shell command; the simplest and most common choice
  2. Shell script — same as command; explicitly calling a .sh file
  3. HTTP handler — posts the event payload to a URL; useful for team-wide policy servers
  4. Prompt handler — asks Claude a yes/no question; useful when the decision requires nuanced judgment that an if statement can’t express
  5. Agent handler — spawns a sub-agent with its own tool access for complex validation

Start with command. Reach for the others only when the decision logic genuinely needs more than an if statement.

The exit-code contract

Every command-type hook lives and dies by its exit code. This is the entire contract:

#!/usr/bin/env bash
# Enforce a $500 refund cap
AMOUNT=$(echo "$HOOK_TOOL_INPUT" | jq -r '.amount')
if (( $(echo "$AMOUNT > 500" | bc -l) )); then
  echo '{"decision":"block","reason":"policy_cap","retriable":false}'
  exit 2
fi
exit 0
  • Exit 0 — allow the tool to run
  • Exit 2 — block the tool; the message written to stdout is returned to the model as the reason
  • Any other exit code — the error is logged and the tool is allowed (so a crashing hook does not silently become a deny — but see the next section)

Six lines of bash can enforce a rule that no amount of prompt engineering can guarantee.

The failure mode you must handle

What happens when the hook itself crashes, times out, or returns malformed JSON? Two possible policies:

Door 1 — silently allow: The tool executes as if no hook existed. Your policy just evaporated and nobody was notified.

Door 2 — deny and log loudly (fail closed): The tool is blocked; an error is surfaced.

Always take door 2. A guarantee that disappears on error was never a guarantee — it was a guarantee-shaped suggestion. Design your hooks to fail closed.

Security: hooks run as you

This is worth saying plainly because it is easy to overlook. A hook is your code running with your privileges. It has access to your filesystem, your network, and — critically — your environment variables, which may include API keys and tokens.

Before adding a hook:

  • Read the entire script before running it
  • Pin any external dependencies to specific versions
  • Treat hook code with the same review rigor you would apply to any privileged server-side code
  • Never copy-paste a hook from an untrusted source without auditing it line by line

A hook that exfiltrates your ANTHROPIC_API_KEY via an HTTP request is not a theoretical concern. It is six lines of bash.

Common misconceptions

  • “Hooks replace the system prompt.” They do not. Hooks are the backstop, not the primary interface. Prompts handle the common path efficiently; hooks handle the cases where failure has real consequences.
  • “Only shell scripts can be hooks.” The command handler is the most common, but HTTP handlers, prompt handlers, and agent handlers all exist for situations where an if statement is not the right tool.
  • “Exit code 1 blocks the tool call.” No. Only exit code 2 blocks and returns a reason. Exit code 1 (or any non-zero code other than 2) is logged and the tool is allowed through. Using the wrong exit code means your policy silently does nothing.
  • “Hooks are sandboxed.” They are not. Hooks run with your full user privileges. Treat them as privileged code accordingly.

Frequently asked questions

Can a hook modify the tool input rather than just allowing or blocking it? The pre_tool_use hook can return modified parameters in its JSON output, not just a block decision. This lets you sanitize or clamp inputs (e.g., cap a refund amount to $500 rather than rejecting it outright), giving the pipeline a chance to self-correct without a round-trip back to the model.

What is the difference between a prompt handler and asking Claude in my shell script? A prompt handler is a first-class hook type that routes the decision through a Claude call with a structured yes/no framing and full awareness of the hook context. Calling Claude from inside a shell script is possible but bypasses this integration, meaning you handle all the API plumbing, error handling, and context injection yourself. Use the prompt handler type when you want Claude-assisted judgment with less boilerplate.

How do per-turn hooks differ from per-tool-use hooks? A per_turn hook fires once per user message, before any tools run. It can block an entire turn — useful for rejecting a category of request outright. A per_tool_use hook fires for each individual tool invocation within a turn. A single turn might trigger a dozen tool calls and therefore a dozen pre_tool_use firings.

Do hooks work in multi-agent (sub-agent) setups? Yes, and Anthropic provides dedicated sub-agent lifecycle events for exactly this use case. This is part of the “11+ additional events” beyond the core four, specifically designed for multi-agent observability so you can enforce policies across the full call graph, not just the top-level agent.

Where this fits in the series

This tutorial is part of the How Claude Actually Works course on The Stack Underflow. Hooks represent layer three of the agent loop’s control surface — the hard enforcement layer that sits beneath prompt guidance and tool descriptions. The next video covers the surfaces where hooks live: Claude Code and the Agent SDK. Browse all tutorials in the series to follow the full progression from tokens to production-grade agent architecture.

Found this useful? The deep version lives on YouTube — new breakdowns of how AI dev tools actually work, weekly.

Subscribe on YouTube →