VIDEO · CLAUDE CODE · HOOKS • 9 min read •

A prompt is a request. A hook is a law.

A rule in a system prompt is a request. The model reads it together with everything else in the conversation, and usually it complies. A rule that must hold every single time belongs in code, at the moment your code holds the tool call and nothing has run yet. In Claude Code and the Claude Agent SDK that moment is a PreToolUse hook. The first episode of my video series on agent architecture shows it on a single refund. This post goes through it with the code from the film.

Episode 1 of Agentic Whiteboard, in English with English captions. Watch it on YouTube

Meet Blob, an AI support agent. At the top of its system prompt sits one rule: NEVER refund more than $500. A customer asks for $2,000, and Blob answers: “Sure! Refunding $2,000 now.” Blob did not break and did not go rogue. It did what a language model does with a prompt.

The episode runs a little over seven minutes. It opens Agentic Whiteboard, my new YouTube channel about agent architecture. Three characters carry the story: Blob, the agent; Ada, the support lead who runs it; and Kai, the architect who built it.

Why a rule in the prompt slips

Kai starts where almost everyone starts. He writes the rule into the system prompt and tests it. Ten requests, then a hundred, and Blob refuses every time. So he ships it.

The model does not execute a system prompt the way a computer executes code. It reads the prompt together with the rest of its context, and then it decides. Usually the rule wins. But the rule is one line. Around it, in the film, there is a customer whose order was a gift for their daughter and arrived broken. The conversation is thirty-seven turns long, and the request is phrased in a way no test tried. The details are invented, but every agent in production has its own.

The exam guide for the Claude Certified Architect Foundations says it directly, in task statement 1.4:

“When deterministic compliance is required (e.g., identity verification before financial operations), prompt instructions alone have a non-zero failure rate”

The usual next move is capital letters. NEVER, MANDATORY, the same rule three times. Maybe the failures get rarer. They do not go to zero. Anthropic's prompting guide adds a warning of its own: prompts that used aggressive wording such as “CRITICAL: You MUST use this tool when…” may make newer models overtrigger. Its fix is to soften the language. A bigger sign is still a sign.

Where the gate goes

Claude never runs the tools you define. When it wants one, it stops with stop_reason: "tool_use" and names the tool and the inputs it chose: refund_customer, order A-1042, amount 2000. Your code runs the tool and sends the result back. I described the whole loop, from the first tool_use block to end_turn in a separate post.

That loop has a gap. The model has decided, your code holds the request, and the money has not moved yet. The gate goes into that gap. It is code that runs before the tool runs. It does not read the conversation. It does not care about the gift or how polite the request was. It reads one thing: the amount. 2000 is more than 500, so the call is denied. Not usually. Every time.

Frame from the episode: Blob carries a $2,000 ticket into a green gate stamped DENIED; the hook reads tool_input.amount = 2000 and ignores the conversation, the gift and how polite it was
From the episode: the gate reads the amount and nothing else.

The PreToolUse hook in the Agent SDK

In Claude Code and the Claude Agent SDK this gate has a name: a PreToolUse hook. It fires after Claude has chosen a tool and its inputs, and before the tool runs. It can let the call through, or deny it and give a reason. Claude sees that reason. Here is Kai's hook in Python, exactly as it appears in the film:

Agent SDK · PreToolUse hook
async def refund_limit(input_data, tool_use_id, context):
    amount = input_data["tool_input"]["amount"]
    if amount > 500:
        return {"hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason":
                "Over $500. Escalate to a human."}}
    return {}

HookMatcher(matcher="mcp__support__refund_customer",
            hooks=[refund_limit], on_failure="block")

The hook itself is nine lines. The HookMatcher call below it decides whether it really holds, and it carries two details.

  • The matcher names one tool. A custom tool in the Agent SDK runs on an in-process MCP server, so its full name is mcp__{server_name}__{tool_name}. Without a matcher the hook fires on every tool. On lookup_order the line that reads ["amount"] raises an error, because that tool has no amount.
  • on_failure="block". By default, when a hook callback throws, the call goes on to the normal permission flow. The guard fails open, and it fails exactly on the case nobody tested. With this option a crashing guard blocks the call. So let the error propagate: a callback that catches its own error and returns {} is not failing. If the SDK cannot honour the option, the docs say the query fails at startup and sends no prompt.

On the Messages API, with your own loop, you do not need a hook system. You already own the gap. Check the input before you run the tool. If it breaks the rule, do not run it. Send back a tool result marked as an error, with the reason in plain words:

Messages API · your own loop
tool_result · is_error: true
"Over $500. Escalate to a human."

Different machinery, same principle: the rule lives in code, at the one point every refund has to pass through. Whether you write that loop yourself or take it from the SDK is a separate choice. I compared what the bare API, the Agent SDK and the CLI each leave to your own code before.

NO PARTNER EMPLOYER?

Anthropic admits to its exams only people from Claude Partner Network organizations. Join our collective of AI consultants and sit all four exams through our partner organization.

Exit code 1 alone does not block

Claude Code can also run a hook as a shell command, and that has one gotcha. To block, the command exits with code 2, or it returns JSON with a deny decision. Exit code 1 on its own does not block. By default it is a non-blocking error, and the call goes ahead.

Claude Code · command hook
exit 2                      → blocked ✓
permissionDecision: "deny"  → blocked ✓
exit 1 alone                → call goes ahead ✗

The hooks reference says it plainly: “If your hook is meant to enforce a policy, use exit 2.” A hook can also be set to block when it fails (onFailure: "block"), and then other exit codes block too. Without that setting, a guard that exits 1 passes the demo and fails in production.

The JSON route has its own trap. For PreToolUse the field is hookSpecificOutput.permissionDecision. The top-level decision field is deprecated for this event. The docs also warn that when a hook exits 0 and prints JSON that fails schema validation, the action goes ahead.

After the deny

So the refund is blocked. A gate that only slams shut is half a design, because Blob is still talking to an upset customer. This is where the reason matters. Kai's hook does not just say no. It says: “Over $500. Escalate to a human.” Kai also gave Blob one more tool, escalate_to_human. Blob calls it with a short summary:

Blob's next tool call
escalate_to_human({
  "order_id": "A-1042",
  "summary": "Broken gift,
    $2,000 requested, over the limit."
})

The case lands with Ada, who can actually approve it. The customer hears the truth: “I can't approve this myself, so I've passed it to a teammate who can.”

Frame from the episode: Blob hands a folder marked Case A-1042, Needs approval, to Ada at her desk, with the caption Lands with a human who can say yes
From the episode: the case goes to the person who can say yes.

Notice what is guaranteed and what is not. The block is guaranteed. What happens next is still the model's choice. So make the good choice easy: a clear reason and the right tool. Not a generic error, not a fake success, not a dead conversation. The Architect Foundations guide explains why a generic error such as “Operation failed” keeps the agent from choosing the right recovery (task statement 2.2), and it calls a silent success and ending the whole workflow anti-patterns (5.3). For the handoff it names four fields a human needs when they cannot see the transcript: customer ID, root cause, refund amount and recommended action.

The $500 limit is not my invention either. Task statement 1.5 of the same guide uses it as its own example: hooks that block refunds exceeding $500 and redirect to human escalation.

Newsletter

The Agentic Architect

Practical patterns, case studies, agentic projects taken apart and AI news. Zero spam, once a week.

You'll get one email to confirm. Unsubscribe with one click. Privacy policy

Prompt or code: one question

Should every rule become a hook? The film sorts five rules with one question: must it hold every single time?

RuleWhere it goes
Be warm, keep answers short.prompt
Summarise every case in three bullets.prompt + example
Verify the customer's identity before any refund.prerequisite gate
Refunds over $500 go to a human.PreToolUse hook
Never drop a production table.hook / deny the tool

Tone and format can slip once and nobody loses money, so they stay in the prompt. The other three must hold. Identity verification is a prerequisite gate: the refund tool refuses to run until verification has succeeded. The production table needs a hook on the tool that runs the SQL, or no write access to production at all, with that tool denied outright.

One trap in Claude Code: putting a tool on the allowed list only means it runs without asking. It does not take the other tools away. If something must never run, deny the tool. A bare tool name in a deny rule removes the tool from Claude's context entirely. A scoped rule matches the command as it is written, so it cannot reliably catch a DROP hidden inside SQL.

The same split opens my twelve design rules for agents, written while I was preparing for the exam. Everything that must always hold goes into code, and the prompt keeps what needs judgment.

When a hook is overkill

Do not turn every sentence of your prompt into code. A hook costs something: code to write, tests to keep green, edge cases to think through. If the model keeps picking the wrong tool, the cheapest fix is usually a clearer tool description. Anthropic's tool documentation calls detailed descriptions “by far the most important factor in tool performance”. If the output format drifts, show an example. Start with the cheapest fix that reaches the root cause.

Frame from the episode: Kai points at two doors under the question slips once?, one labelled BUG leading to a better prompt and one labelled DISASTER leading to a hook
From the episode: Kai's test for any rule.

Kai's test fits in two sentences. Imagine the rule slips once. If that is a bug, write a better prompt; if it is a disaster, write a hook.

If you are studying for the Claude Certified Architect Foundations exam, keep one thing from this episode. When a question says a rule must always hold, strengthening the system prompt is the trap. Enforce the rule in code.

Where this comes from. The technical details come from Anthropic's documentation for Claude Code, the Agent SDK and the Claude API, and from the Claude Certified Architect Foundations exam guide, version 1.0 of July 2026. A second agent checked the film's narration and on-screen code against those sources on 6 November 2026, and the code above is the checked version. Blob, Ada, Kai and the refund story are invented. Agentic Whiteboard and the Certified AI Consultant Program are independent, not affiliated with or endorsed by Anthropic.

SP

Szymon Paluch

Claude Certified Architect · ex-CTO

Preparing for a Claude certification?

Our prep program goes deeper, with practice scenarios.

See the program

New episodes go up on Agentic Whiteboard on YouTube.

Related posts