What you will be able to do
- Explain why a prompt instruction cannot guarantee workflow ordering, and when that matters
- Design a PreToolUse prerequisite gate that blocks process_refund until get_customer has returned a verified customer ID
- Write one hook matcher that covers a family of sensitive tools without duplicating hook code
Key concept
Programmatic enforcement — Code outside the model, such as a hook, runs on a lifecycle event and can allow or deny a tool call no matter what the model decided. Prompt guidance only asks the model to behave, so the outcome depends on how the model interprets it.
1.Two ways to control ordering: asking and enforcing
A customer-support agent has an ordering rule: verify the customer's identity, then touch money. You can express that rule in two ways. The first is prompt-based guidance: you write it into the system prompt, for example "Always call get_customer before process_refund." The second is programmatic enforcement: code that runs outside the model checks each tool call and refuses the ones that break the rule.
Prompt guidance works, and it works most of the time. The tool-use documentation says the model's tool-calling behaviour "is steerable through your system prompt" and that stronger phrasing pushes it further. The same page is candid about the limit: a model may fill in values you never supplied, and "This behavior is not guaranteed, especially for more ambiguous prompts and for less capable models." So an instruction lowers the failure rate. It does not bring it to zero.
| Aspect | Hook | Skill (instructions Claude follows) |
|---|---|---|
| Runs | A shell command, HTTP request, MCP tool call, LLM prompt, or subagent | Instructions Claude reads and follows |
| Triggered by | Lifecycle events such as PostToolUse or SessionStart | You typing /<name>, or Claude matching the description to your task |
| Determinism | Always fires on its event; the trigger is guaranteed | Claude interprets the instructions; outcome can vary |
| Best for | Linting after edits, blocking unsafe commands, logging, notifications | Workflows that need reasoning, reference material, multi-step tasks |
The deciding question is what a single failure costs. If the agent occasionally forgets to cite a knowledge-base article, a prompt is enough. If it occasionally refunds money to someone whose identity it never checked, one failure in a thousand is still a compliance incident. When compliance must be deterministic, such as identity verification before a financial operation, the rule belongs in code. The prompt can still explain the rule so the model usually follows it on the first try. The code makes sure breaking it has no effect.
There is also a middle option. The tool-use overview notes: "To require a tool call rather than rely on prompting, set tool_choice." That forces the model to call a tool on a given turn. It does not express a condition like "only after a verified customer exists," which depends on what earlier calls returned. A gate that can inspect each call is the tool for that.
No. 500 passes show a low failure rate, not a zero one. The docs describe prompted behaviour as steerable but not guaranteed, especially on ambiguous inputs, which are the ones testing is least likely to cover. Enforcement means code that rejects the call whatever the model decides.
2.Building a prerequisite gate with PreToolUse
Hooks are the enforcement mechanism in both Claude Code and the Agent SDK. The Agent SDK hooks page lists what they are for, starting with: "Block dangerous operations before they execute, like destructive shell commands or unauthorized file access". The event that makes blocking possible is PreToolUse, which the hooks reference describes as "Before a tool call executes. Can block it". It fires on every tool call inside the agentic loop, together with PostToolUse.
The flow is fixed: an event fires, the SDK collects registered hooks, matchers filter which hooks run, callbacks execute, and "Your callback returns a decision". In the SDK example below, the callback reads the tool's input and returns a deny decision with a reason. Returning an empty object allows the call.
async def protect_env_files(input_data, tool_use_id, context):
# Extract the file path from the tool's input arguments
file_path = input_data["tool_input"].get("file_path", "")
file_name = file_path.split("/")[-1]
# Block the operation if targeting a .env file
if file_name == ".env":
return {
"hookSpecificOutput": {
"hookEventName": input_data["hook_event_name"],
"permissionDecision": "deny",
"permissionDecisionReason": "Cannot modify .env files",
}
}
# Return empty object to allow the operation
return {}A prerequisite gate uses the same shape but checks state as well as input. The pattern has two parts:
1. Record the prerequisite. A PostToolUse hook, which fires after a tool call succeeds, sees get_customer's result. If the result contains a verified customer ID, it stores that ID in your application's session state.
2. Check it downstream. A PreToolUse hook on process_refund looks up that state. If no verified ID exists, or the refund targets a different customer, it returns deny with a reason such as "Customer identity not verified; call get_customer first."
The reason matters. The model reads it and can correct course, in the same way that a tool_result marked is_error lets Claude "incorporate this error into its response to the user." A gate with a clear reason blocks the unsafe call and also points the agent at the missing step. The ordering is then guaranteed by code, and the prompt only makes it efficient.
3.One gate for a family of tools: matchers
Real systems rarely have a single sensitive tool. Refunds, credits and voided authorizations all move money, and all need the same identity check. Writing one hook per tool name duplicates code, and it quietly fails open the day someone adds a fourth money-moving tool and forgets the hook.
Matchers solve this. Registration pairs a matcher pattern with a list of callbacks, and the SDK example matches two tools with one pattern, commenting that "The matcher filters to only Write and Edit tool calls":
options = ClaudeAgentOptions(
hooks={
# Register the hook for PreToolUse events
# The matcher filters to only Write and Edit tool calls
"PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])]
}
)The Claude Code settings example uses the same form with "matcher": "Bash|PowerShell". For a billing server, list exactly the money-moving tools, joined with |. This covers the whole family in one registration and leaves read-only lookups from the same server unmatched, so they run without the gate.
Two more properties make hooks suitable for compliance. First, "Hooks merge: all registered hooks fire for their matching events regardless of source." A project-level gate can't be switched off by a user-level hook that doesn't mention it. Second, a hook that makes no decision falls through: the Claude Code example exits 0 with the comment "no decision; normal permission flow applies". So a gate only has to decide the cases it owns.
A refund workflow requires that process_refund always be called with the exact customer_id captured by an earlier verified get_customer call, never a value the model retypes from the conversation. A PreToolUse hook already blocks the call when no verified ID exists in session state. What should the hook do once a verified ID is present, to prevent the model from substituting a different ID string?
Correct answer: B — Return permissionDecision "allow" together with updatedInput that overwrites the tool's customer_id argument with the verified ID stored in session state
- A. Incorrect. An empty object allows the call with whatever customer_id the model supplied, which is exactly the untrusted value the workflow needs to prevent from reaching the tool.
- B. Correct. Combining "allow" with updatedInput lets the hook substitute the trusted, verified value for whatever the model passed, closing the gap between what was verified and what actually reaches the tool.
- C. Incorrect. Escalating every already-verified refund to manual retyping adds friction without fixing the substitution risk, and does not correct the argument itself.
- D. Incorrect. Defer ends the query for later resumption; it does not correct the argument and is unnecessary once the ID is already verified in session state.
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.A firm enough system-prompt instruction ("ALWAYS verify identity before refunding") makes the ordering deterministic.Why is that wrong?
Prompts steer the model's tool use, but the docs state the behaviour is not guaranteed. Deterministic ordering needs a hook that can block the call.
Covered in Two ways to control ordering: asking and enforcing
2.Gating several sensitive tools requires one PreToolUse hook registration per tool name.Why is that wrong?
A matcher can name several tools with alternation, so one registration and one callback cover the whole family. Unlisted read-only tools stay ungated.
Covered in One gate for a family of tools: matchers
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“This behavior is not guaranteed, especially for more ambiguous prompts and for less capable models.”
↩︎ Two ways to control ordering: asking and enforcing“To require a tool call rather than rely on prompting, set tool_choice.”
↩︎ Two ways to control ordering: asking and enforcing“This behavior is not guaranteed, especially for more ambiguous prompts and for less capable models.”
↩︎ Exam trap 1 - 2.https://code.claude.com/docs/en/features-overviewOfficial docs
“Claude interprets the instructions; outcome can vary”
↩︎ Two ways to control ordering: asking and enforcing“Hooks merge: all registered hooks fire for their matching events regardless of source.”
↩︎ One gate for a family of tools: matchers“Always fires on its event; the trigger is guaranteed”
↩︎ Key concept - 3.https://code.claude.com/docs/en/agent-sdk/hooksOfficial docs
“Block dangerous operations before they execute, like destructive shell commands or unauthorized file access”
↩︎ Building a prerequisite gate with PreToolUse“The matcher filters to only Write and Edit tool calls”
↩︎ One gate for a family of tools: matchers“The matcher filters to only Write and Edit tool calls”
↩︎ Exam trap 2 - 4.https://code.claude.com/docs/en/hooksOfficial docs
“Before a tool call executes. Can block it”
↩︎ Building a prerequisite gate with PreToolUse“no decision; normal permission flow applies”
↩︎ One gate for a family of tools: matchers - 5.
“Claude will then incorporate this error into its response to the user.”
↩︎ Building a prerequisite gate with PreToolUse