๐Ÿ“š Domain 1 ยท Task Statement 1.5

Apply Agent SDK Hooks for Tool Call Interception & Data Normalization

๐Ÿ“Š Domain Weight: 27% โญ High Exam Priority ๐Ÿ”— Scenario: Customer Support Agent

Hooks are the enforcement layer of the Agent SDK. While Claude reasons about what to do, hooks control whether it is allowed to do it and what data it sees when it does. This task statement tests two distinct hook patterns: PostToolUse hooks for normalizing heterogeneous data after a tool returns, and PreToolCall hooks for enforcing compliance rules before a tool executes. The central exam principle: hooks give deterministic guarantees; prompt instructions give probabilistic compliance.

๐Ÿ“‹ Contents

  1. Analogy โ€” The Airport Customs Officer
  2. The Two Hook Types: PostToolUse & PreToolCall
  3. Deterministic vs Probabilistic Compliance
  4. PostToolUse Hooks โ€” Data Normalization
  5. PreToolCall Hooks โ€” Compliance Enforcement
  6. Blocking + Redirecting to Alternative Workflows
  7. When to Choose Hooks vs Prompt Instructions
  8. Anti-Patterns & Pro Tips
  9. Summary & Exam Key Points

โœˆ๏ธ Analogy โ€” The Airport Customs Officer & Delegation Boundaries

โœˆ๏ธ Analogy โ€” Two Roles of the Customs Officer

At international arrivals, the customs officer plays two distinct roles, which perfectly illustrate the AI Fluency concept of Delegation Boundaries. Role 1 (PostToolUse): When luggage arrives on the conveyor belt from different countries, it gets processed through X-ray and converted into a standardised inspection format โ€” regardless of which airline or origin it came from. The inspector always sees the same structured view. Role 2 (PreToolCall): Before any goods may leave the secure area, the officer checks a manifest. If the declared value exceeds the duty-free limit, the passenger is stopped and redirected to the customs desk โ€” they cannot just walk through.

PostToolUse hooks are the X-ray normalizer โ€” they ensure the agent's Description of reality is formatted correctly. PreToolCall hooks are the definitive bounds of Delegation โ€” they intercept outgoing tool calls and enforce absolute limits on the agent's autonomy, blocking any action that exceeds its designated authority.

๐Ÿช The Two Hook Types: PostToolUse & PreToolCall

The Agent SDK intercepts tool execution at two points. Understanding where each hook fires is the foundation of this entire task statement.

Figure 1 โ€” Hook Interception Points in the Agentic Loop
CLAUDE tool_use request PRE-TOOL HOOK Fires BEFORE tool runs Allow or Block โ›” Block โ†’ return error โœ“ Allow TOOL executes & returns POST-TOOL HOOK Fires AFTER tool returns Normalize / transform Normalized result returned to Claude for next reasoning step
Hook TypeFires WhenPrimary PurposeCan Block?
PreToolCallBEFORE the tool executesEnforce compliance rules; block policy-violating callsYes โ€” returns error as tool_result
PostToolUseAFTER the tool returns its resultNormalize heterogeneous data formats before Claude reads themNo โ€” transforms but does not block

โš–๏ธ Deterministic vs Probabilistic Compliance

This is the central conceptual distinction for Task 1.5. The exam will present scenarios and ask: which approach guarantees compliance? The answer is always hooks, never prompts.

DimensionPrompt-Based InstructionsSDK Hook Enforcement
Compliance rateProbabilistic โ€” non-zero failure rate even with perfect promptsDeterministic โ€” guaranteed 100% of the time
Can user bypass?Yes โ€” persuasive context or jailbreak attempts may succeedNo โ€” hook fires in your code, outside Claude's reasoning
Audit trailWeak โ€” depends on Claude's output textStrong โ€” hook logs every check with timestamp
Implementation layerSystem prompt / user promptApplication code (SDK hook callbacks)
Best forSoft guidelines, stylistic preferencesFinancial thresholds, regulatory compliance, irreversible actions
Failure modeClaude reasons around instruction given compelling contextBug in hook logic โ†’ false block or false pass
โš ๏ธ The Exam's Central Test

When a scenario asks: "What is the most reliable way to ensure refunds above $500 always require human approval?" โ€” the correct answer is always a PreToolCall hook that intercepts process_refund calls and checks the amount before execution. A system prompt instruction saying "never approve refunds above $500 without human review" is explicitly wrong because it has a non-zero failure rate.

๐Ÿ”„ PostToolUse Hooks โ€” Data Normalization

When your agent uses multiple MCP tools from different backends, those tools inevitably return data in heterogeneous formats. Dates might be Unix timestamps from one tool and ISO 8601 strings from another. Status codes might be numeric integers from a legacy system and descriptive strings from a modern API. Without normalization, Claude must reason across these inconsistencies โ€” increasing error risk and token usage.

The PostToolUse hook fires after every tool call, receiving the raw tool result before Claude processes it. It transforms the output into a normalized format and returns the cleaned version to the agentic loop.

The Three Heterogeneous Format Problems (From the Exam Guide)

๐Ÿ• Unix Timestamps

Tool A returns 1711929600. Tool B returns "2024-04-01T12:00:00Z". Claude must compare them โ€” but they're incomparable without normalization. Hook converts all timestamps to ISO 8601.

๐Ÿ“‹ Numeric Status Codes

Legacy system returns {"status": 2}. Modern system returns {"status": "shipped"}. A PostToolUse hook maps 2 โ†’ "shipped" so Claude always sees human-readable strings.

๐Ÿ“… Mixed Date Formats

One MCP tool returns "April 1, 2024". Another returns "01/04/2024". Another returns "2024-04-01". Hook normalizes all to ISO 8601 before Claude sees any of them.

Python โ€” PostToolUse Normalization Hook
from anthropic_agent_sdk import AgentHook, ToolResultContext
import datetime

# Status code mapping for legacy order management system
ORDER_STATUS_MAP = {
    0: "pending",
    1: "confirmed",
    2: "shipped",
    3: "delivered",
    4: "cancelled",
    5: "refunded"
}

class DataNormalizationHook(AgentHook):
    """PostToolUse hook โ€” normalizes heterogeneous data from multiple MCP tools."""

    async def post_tool_use(self, ctx: ToolResultContext):
        tool_name = ctx.tool_name
        result = ctx.result  # Raw tool output (dict)

        # โ”€โ”€ Normalize lookup_order results โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        if tool_name == "lookup_order":
            # 1. Convert Unix timestamp โ†’ ISO 8601
            if "created_at" in result and isinstance(result["created_at"], int):
                result["created_at"] = datetime.datetime.utcfromtimestamp(
                    result["created_at"]
                ).isoformat() + "Z"

            # 2. Translate numeric status code โ†’ human-readable string
            if "status" in result and isinstance(result["status"], int):
                result["status"] = ORDER_STATUS_MAP.get(
                    result["status"], f"unknown_status_{result['status']}"
                )

        # โ”€โ”€ Normalize get_customer results โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        if tool_name == "get_customer":
            # Normalize "April 1, 2024" or "01/04/2024" โ†’ "2024-04-01"
            if "member_since" in result:
                result["member_since"] = _normalize_date(result["member_since"])

        # Return normalized result โ€” Claude will now reason on consistent data
        return ctx.with_result(result)


def _normalize_date(raw: str) -> str:
    """Attempt to parse multiple date formats and return ISO 8601."""
    for fmt in ("%B %d, %Y", "%d/%m/%Y", "%Y-%m-%d"):
        try:
            return datetime.datetime.strptime(raw, fmt).strftime("%Y-%m-%d")
        except ValueError:
            continue
    return raw  # Return original if unparseable
โญ Pro Tip โ€” Normalize Silently, Log Verbosely

PostToolUse hooks should normalize data silently from Claude's perspective โ€” the agent sees clean data and never knows transformation occurred. But your hook should log every transformation with the original and normalized values for debugging. This creates an audit trail and makes it easy to identify when a new tool returns an unexpected format that your hook doesn't yet handle.

๐Ÿ”’ PreToolCall Hooks โ€” Compliance Enforcement

PreToolCall hooks intercept outgoing tool calls before the tool executes. This is the correct mechanism for enforcing business rules that must never be bypassed regardless of what Claude reasons. The hook receives the tool name and input parameters, checks against your policy, and either allows the call to proceed or returns an error as a tool_result back to Claude.

๐Ÿ’ก Exam Guide Reference โ€” The Canonical Example

The exam guide states: "Implementing tool call interception hooks that block policy-violating actions (e.g., refunds exceeding $500) and redirect to alternative workflows (e.g., human escalation)."

This is the primary test scenario: a customer requests a refund. Claude calls process_refund(amount=750). The PreToolCall hook fires, checks amount > 500, blocks the call, and returns an error that redirects to escalate_to_human.

Python โ€” PreToolCall Compliance Enforcement Hook
from anthropic_agent_sdk import AgentHook, ToolCallContext

# Business policy constants โ€” single source of truth
REFUND_AUTO_APPROVAL_LIMIT = 500.00  # USD
BULK_CANCEL_ITEM_LIMIT = 10

class ComplianceEnforcementHook(AgentHook):
    """PreToolCall hook โ€” enforces business rules before any tool executes."""

    async def pre_tool_call(self, ctx: ToolCallContext):
        tool_name = ctx.tool_name
        tool_input = ctx.tool_input  # The parameters Claude wants to pass

        # โ”€โ”€ Rule 1: Refunds above threshold require human approval โ”€โ”€โ”€โ”€โ”€โ”€
        if tool_name == "process_refund":
            amount = tool_input.get("amount", 0)

            if amount > REFUND_AUTO_APPROVAL_LIMIT:
                # Block the refund and tell Claude WHY + what to do instead
                return ctx.block(
                    error=f"POLICY_VIOLATION: Refund of ${amount:.2f} exceeds the "
                          f"${REFUND_AUTO_APPROVAL_LIMIT:.2f} auto-approval limit. "
                          f"Use escalate_to_human with reason='large_refund' and "
                          f"include: customer_id, refund_amount={amount}, "
                          f"root_cause, and recommended_action in the handoff."
                )

        # โ”€โ”€ Rule 2: Bulk cancellations above limit require human review โ”€
        if tool_name == "cancel_orders":
            order_ids = tool_input.get("order_ids", [])

            if len(order_ids) > BULK_CANCEL_ITEM_LIMIT:
                return ctx.block(
                    error=f"POLICY_VIOLATION: Bulk cancellation of {len(order_ids)} orders "
                          f"exceeds the {BULK_CANCEL_ITEM_LIMIT} order limit for automated "
                          f"processing. Escalate to human for review."
                )

        # โ”€โ”€ Allow all other tool calls to proceed unmodified โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        return ctx.allow()
โœ… Key Mechanic โ€” Error as tool_result

When a PreToolCall hook blocks a tool call, the SDK returns the error message as a tool_result in the conversation. Claude receives this, reads the error text, and decides its next action โ€” typically calling escalate_to_human as instructed in the error message. This is why actionable error messages matter: they tell Claude exactly what to do next, preventing hallucinated alternative actions.

๐Ÿ”€ Blocking + Redirecting to Alternative Workflows

A well-designed PreToolCall hook doesn't just block โ€” it redirects Claude to the correct alternative workflow. The error message returned as tool_result should be prescriptive, not just descriptive. Compare these two approaches:

โŒ Blocking Without Redirection

"POLICY_VIOLATION: Refund amount exceeds limit."

Claude reads this, doesn't know what to do, and may attempt a workaround โ€” breaking the refund into two smaller calls, or hallucinating an explanation to the customer.

โœ… Blocking With Explicit Redirection

"POLICY_VIOLATION: Refund of $750 exceeds $500 auto-limit. Use escalate_to_human with reason='large_refund'. Include: customer_id, refund_amount=750, root_cause, recommended_action."

Claude now knows the exact next action and the exact handoff fields required.

The Full Redirect Flow in the Customer Support Agent

Figure 2 โ€” PreToolCall Block โ†’ Redirect to Human Escalation
1. Claude calls process_refund(750) 2. PreToolCall fires amount(750) > limit(500) โ›” ctx.block(error=...) "use escalate_to_human" error returned as tool_result 3. Claude reads error 4. Claude calls escalate_to_human(...) 5. Human agent receives handoff: customer_id, amount, root_cause, action

๐ŸŽฏ When to Choose Hooks vs Prompt Instructions

The exam will present scenarios and ask you to select the correct enforcement mechanism. Use this decision framework:

ScenarioUse Hooks?Reasoning
Refunds above $500 must go to human approvalโœ… Yes โ€” PreToolCallFinancial threshold โ€” zero-failure-rate required. Prompt has non-zero failure rate.
Normalize Unix timestamps from legacy toolโœ… Yes โ€” PostToolUseSystematic format issue across all calls โ€” hook normalizes once, not per-prompt.
Claude should use a formal, professional toneโŒ No โ€” PromptStylistic preference, non-critical. Probabilistic compliance acceptable.
Cancellations of more than 10 orders require reviewโœ… Yes โ€” PreToolCallBusiness rule with regulatory implications. Must be deterministic.
Agent should summarise results conciselyโŒ No โ€” PromptFormatting preference โ€” acceptable to use prompt guidance.
KYC must complete before account fundingโœ… Yes โ€” PreToolCallRegulatory compliance gate โ€” cannot be bypassed. Use hook + database check.
Tool returns ISO 8601 already โ€” no normalization neededNo hook neededHomogeneous format โ€” PostToolUse hook adds overhead without benefit.
๐Ÿ’ก The Decision Rule (Memorise This)

If the business rule must NEVER be violated, regardless of Claude's reasoning โ†’ Use a hook.
If the preference is directional but some deviation is acceptable โ†’ Use a prompt instruction.
The litmus test: "Would I accept 99% compliance or must it be 100%?" 100% โ†’ hook. <100% โ†’ prompt.

โš ๏ธ Anti-Patterns & Pro Tips

โŒ Compliance Rules in System Prompts Only

Writing "Never process refunds above $500 without human approval" in the system prompt. A persuasive user message can override this. The exam explicitly tests this anti-pattern.

โŒ PostToolUse Hook That Blocks

PostToolUse hooks transform data โ€” they don't block. If you need to block based on a tool's result, you need a different pattern (e.g., a validation step or a second hook). Confusing PostToolUse and PreToolCall is a tested anti-pattern.

โŒ Vague Block Error Messages

"Policy violation." โ€” Claude reads this and has no idea what policy was violated or what to do next. This causes Claude to hallucinate an explanation or attempt workarounds.

โŒ Normalizing When Formats Are Already Consistent

Adding a PostToolUse hook to normalize data that is already in a consistent format adds latency and complexity. Only normalize when multiple tools actually return heterogeneous formats.

โœ… Actionable Block Error Messages

Include: what was violated, the exact threshold, the correct alternative action, and the required handoff fields. Claude uses this to take the right next action immediately.

โœ… Business Constants in One Place

Define thresholds (REFUND_LIMIT = 500) as named constants at the top of your hook, not as magic numbers inline. When policy changes, update in one place.

โœ… Log Every Hook Decision

Log all block/allow decisions with tool name, input values, policy checked, and outcome. This creates an immutable compliance audit trail โ€” critical for financial regulations.

โœ… Test Hooks With Boundary Values

Test with amount=499.99 (should allow), amount=500.00 (check your > vs >= boundary), and amount=500.01 (should block). Off-by-one errors in compliance rules are production incidents.

๐Ÿ“ Summary & Exam Key Points

๐ŸŽฏ Exam Scenario โ€” Customer Support Resolution Agent

The primary exam scenario: "You are building a customer support resolution agent with tools: get_customer, lookup_order, process_refund, escalate_to_human. The agent connects to legacy and modern MCP backends that return data in inconsistent formats. Your company policy requires human approval for refunds above $500."


Questions test: (1) which hook type normalizes tool output before Claude sees it (PostToolUse), (2) which hook type blocks policy-violating tool calls (PreToolCall), (3) why a prompt instruction alone is insufficient for the $500 rule (non-zero failure rate), and (4) what a block error message should contain to correctly redirect Claude (exact alternative action + handoff fields).

1
PostToolUse hooks fire AFTER a tool returns its result. They transform the raw output before Claude sees it. Use them to normalize heterogeneous formats: Unix timestamps โ†’ ISO 8601, numeric status codes โ†’ strings, inconsistent date formats โ†’ standardized format. They cannot block tool execution.
2
PreToolCall hooks fire BEFORE a tool executes. They receive the tool name and input parameters. They can allow or block the tool call. When blocking, they return a structured error as tool_result that Claude reads to determine its next action.
3
Hooks provide deterministic compliance; prompt instructions provide probabilistic compliance. When the exam asks for the "most reliable" or "guaranteed" enforcement mechanism, the answer is always a hook โ€” not a system prompt instruction. Prompt instructions have a non-zero failure rate even when perfectly written.
4
The canonical PreToolCall example: blocking process_refund when amount > $500 and redirecting to escalate_to_human. The block error message must specify: the reason, the threshold violated, the exact alternative tool to call, and the handoff fields required (customer_id, refund_amount, root_cause, recommended_action).
5
Block error messages must be actionable, not generic. "Policy violation" is wrong. "Refund of $750 exceeds $500 auto-limit. Use escalate_to_human with reason='large_refund' and include: customer_id, refund_amount, root_cause, recommended_action." is correct. Claude uses the error text to choose its next action.
6
Choose hooks over prompt instructions when: (a) zero-failure-rate is required, (b) the rule involves financial thresholds or regulatory compliance, (c) the action is irreversible, (d) the system must be tamper-resistant to persuasive user context. Choose prompt instructions for stylistic preferences and soft guidelines.
7
The three heterogeneous data formats the exam tests for PostToolUse normalization: Unix timestamps (integer โ†’ ISO 8601), numeric status codes (integer โ†’ descriptive string), and mixed date format strings (varied locale formats โ†’ standardized ISO 8601). All three appear in the Customer Support Agent scenario.
8
Hooks vs gates distinction: Hooks (Task 1.5) intercept at the tool-call level for data transformation and policy enforcement. Gates (Task 1.4) intercept at the workflow-stage level for prerequisite enforcement. Both are code-level, not prompt-level. Both are deterministic. Use both together in production systems.