Study guides / CCAR-F / Domain 1

Agentic Architecture & Orchestration · Lesson 5 of 7

1.5 - Agent SDK Hooks

Apply Agent SDK hooks for data normalisation and deterministic policy enforcement using the PostToolUse and PreToolUse hook events.

Agent SDK hooks inject deterministic behaviour into an otherwise probabilistic system. They sit right at the boundary between the model's decisions and the real world, intercepting tool calls and results to enforce business rules and normalise data. Remember the enforcement spectrum from 1.4? Hooks are how you implement its programmatic side in practice.

Two Types of Hooks

The Agent SDK provides hooks at two points in the tool execution lifecycle:

PostToolUse hooks run after a tool executes but before the model processes the result. They intercept tool results and transform them before the model sees them. The model receives clean, normalised data regardless of which tool produced it.

PreToolUse hooks (sometimes described as tool-call interception) run before a tool executes. They intercept the outgoing tool call and can block it, modify it, or redirect it to an alternative workflow. The tool never runs if the hook decides to block it.

Key Concept

PostToolUse hooks transform data after execution. PreToolUse hooks enforce policy before execution. Know which direction each hook operates in - the exam tests this distinction.

PostToolUse Hooks: Data Normalisation

Different MCP tools return data in different formats. A customer database might return Unix timestamps (1710489600). An order management system might return ISO 8601 dates ("2024-03-15T12:00:00Z"). A status API might return numeric codes (200, 404, 500) while another returns strings ("active", "cancelled", "pending").

Without normalisation, the model has to interpret these mixed formats on every single iteration. That breeds inconsistency. It might parse a Unix timestamp correctly one time and misread it the next.

A PostToolUse hook solves this by normalising all formats before the model processes them:

The model receives clean, consistent data every time, regardless of which tool or backend system produced it.

PreToolUse Hooks: Policy Enforcement

PreToolUse hooks are the implementation mechanism for the prerequisite gates described in 1.4. They intercept outgoing tool calls before execution and apply business rules:

Use case: Refund threshold enforcement. A hook intercepts all calls to process_refund. If the refund amount exceeds $500, the hook blocks the call and redirects to a human escalation workflow. The refund tool never executes - the hook prevents it before it can run.

Use case: Compliance prerequisite gates. A hook intercepts calls to transfer_funds. If the required anti-money laundering (AML) check has not been completed for this session, the hook blocks the call and returns an error message directing the agent to complete the AML check first.

Use case: Manager approval workflow. A hook intercepts calls to approve_discount for discounts above 20%. The hook pauses execution and routes the request to a manager approval queue. Only after manager approval does the tool execute.

Exam Trap

The exam will present PostToolUse hooks as a solution for blocking policy-violating actions. This is wrong. PostToolUse runs after execution - by the time it fires, the non-compliant action has already occurred. Use PreToolUse hooks (pre-execution) to block actions before they happen.

The Decision Framework

This framework is the core mental model for the exam:

Requirement Mechanism Guarantee
Must be followed 100% of the time Hooks Deterministic
Preferred but occasional deviation is acceptable Prompts Probabilistic

If the business would lose money from a single failure → use a hook. If the business would face legal risk from a single failure → use a hook. If it is a formatting preference or style guideline → prompt-based guidance is fine.

The exam consistently presents prompt-based solutions as distractors for scenarios requiring deterministic enforcement. The decision is not about whether prompts are "good enough" - it's about whether the consequence of a single failure justifies deterministic guarantees.

Hooks vs Prompts: Side-by-Side Comparison

Scenario: International transfers must pass AML checks.

Scenario: Responses should be formatted in markdown.

Scenario: Refunds above $500 require human approval.

Practical Example: Data Format Chaos

A customer support agent uses three MCP tools:

  1. get_customer returns dates as Unix timestamps and status as numeric codes.
  2. lookup_order returns dates as ISO 8601 strings and status as English strings.
  3. check_shipping returns dates as "DD/MM/YYYY" and status as single-character codes ("S" for shipped, "P" for pending).

Without a PostToolUse hook, the model must interpret three different date formats and three different status representations on every iteration. Sometimes it correctly converts a Unix timestamp; sometimes it confuses the day/month order in "DD/MM/YYYY"; sometimes it misinterprets "P" as "processed" instead of "pending."

With a PostToolUse hook, all tool results are normalised before the model sees them:

The model always receives consistent data, eliminating interpretation errors entirely.

Exam traps

Practice question

An agent occasionally processes international transfers without required compliance checks. The compliance team requires 100% enforcement of anti-money laundering (AML) checks before any international transfer is executed. The current system uses prompt instructions that work approximately 95% of the time. What is the correct approach?

  • A Implement a PreToolUse hook that blocks the transfer_funds tool from executing until aml_check returns a verified pass result Correct

    A PreToolUse hook intercepts the outgoing tool call before execution and physically blocks it until the AML check passes. This provides the deterministic 100% guarantee that regulatory compliance demands. No transfer can execute without verification.

  • B Add detailed AML check instructions to the system prompt with examples of correct behaviour and explicit warnings about penalties for non-compliance

    Enhanced prompt instructions may improve the rate from 95% to 97-98% but cannot reach 100%. With AML regulations, even a single missed check can result in significant legal penalties. Probabilistic improvement is insufficient for regulatory requirements.

  • C Add a PostToolUse hook that flags any completed transfer which skipped its AML check and queues it for manual review by the compliance team

    PostToolUse hooks run after execution. By the time the hook detects the missing AML check, the non-compliant transfer has already been processed. Regulatory compliance requires prevention, not post-hoc detection.

  • D Train the agent with few-shot examples demonstrating the correct AML verification workflow before every transfer

    Few-shot examples improve accuracy but remain probabilistic. They cannot guarantee 100% compliance. Regulatory requirements for AML checks demand deterministic enforcement that only hooks can provide.

Build exercise: Implement Agent SDK Hooks for Normalisation and Policy Enforcement

Advanced · 60 minutes

You'll practice:

  1. Create an agent with three MCP tools that return data in different formats: Tool A returns Unix timestamps and numeric status codes, Tool B returns ISO 8601 dates and string statuses, Tool C returns DD/MM/YYYY dates and single-character status codes

    This recreates the data format chaos example from the exam. Without normalisation, the model must interpret three different date formats and three different status representations, leading to inconsistent parsing across iterations.

    You should see: Three tool implementations that each return data with distinct date and status formats. Tool A uses epoch seconds and numeric codes, Tool B uses ISO strings and English statuses, Tool C uses DD/MM/YYYY and single characters.

    Hints
    1. Each tool should return a structured object with at least a date field and a status field, but in its own format.
    2. Define three mock tool handlers that return objects with created_at and status fields, each using a different format convention for both fields.
    3. function toolAHandler(): Record<string, unknown> {
        return { customer_id: "C-001", created_at: 1710489600, status: 200 };
      }
      function toolBHandler(): Record<string, unknown> {
        return { order_id: "ORD-42", created_at: "2024-03-15T12:00:00Z", status: "active" };
      }
      function toolCHandler(): Record<string, unknown> {
        return { shipment_id: "SHP-7", created_at: "15/03/2024", status: "S" };
      }
  2. Implement a PostToolUse hook that intercepts all tool results and normalises dates to ISO 8601 format and status codes to human-readable English strings

    PostToolUse hooks run after execution but before the model processes the result. This is the correct hook direction for data normalisation - the exam tests whether you know that PostToolUse transforms data after execution, not before.

    You should see: A hook function that detects the format of each field and converts it: Unix timestamps to ISO 8601, DD/MM/YYYY to ISO 8601, numeric status codes to English strings, and single-character codes to full words.

    Hints
    1. PostToolUse hooks receive the tool result and return a transformed version. What transformations do you need for each format?
    2. Detect format by type checking: if date is a number, treat as Unix timestamp. If it matches DD/MM/YYYY pattern, parse accordingly. Map numeric statuses (200 = active) and character statuses (S = shipped, P = pending).
    3. const postToolUseHook = (toolName: string, result: Record<string, unknown>) => {
        const normalised = { ...result };
        // Normalise dates
        if (typeof result.created_at === "number") {
          normalised.created_at = new Date(result.created_at * 1000).toISOString();
        } else if (typeof result.created_at === "string" && result.created_at.match(/^\d{2}\/\d{2}\/\d{4}$/)) {
          const [day, month, year] = result.created_at.split("/");
          normalised.created_at = new Date(`${year}-${month}-${day}`).toISOString();
        }
        // Normalise status
        const statusMap: Record<string, string> = { "200": "active", "404": "not_found", "S": "shipped", "P": "pending" };
        if (statusMap[String(result.status)]) {
          normalised.status = statusMap[String(result.status)];
        }
        return normalised;
      };
  3. Verify the model receives consistent data by testing with queries that require results from all three tools

    Consistent data eliminates interpretation errors. Without normalisation, the model might confuse day/month order in DD/MM/YYYY or misinterpret status code P as processed instead of pending. Verification proves the hook works across all tool outputs.

    You should see: Three tool results that all use ISO 8601 dates and English status strings, regardless of which tool produced them. The model response should reference dates and statuses consistently without confusion.

    Hints
    1. Run a query that forces all three tools to be called. Check that the model sees normalised data, not raw formats.
    2. Send a prompt that asks about a customer, their order, and the shipment status. Inspect the data the model receives after the PostToolUse hook processes each result.
    3. const testPrompt = "Look up customer C-001, find their order ORD-42, and check shipment SHP-7 status.";
      const response = await runAgentWithHooks(testPrompt);
      // Inspect hook output logs:
      // Tool A: created_at should be "2024-03-15T12:00:00.000Z" not 1710489600
      // Tool C: status should be "shipped" not "S"
      console.log("All dates ISO 8601:", hookOutputs.every(o => o.created_at.includes("T")));
      console.log("All statuses readable:", hookOutputs.every(o => typeof o.status === "string" && o.status.length > 1));
  4. Add a PreToolUse hook that blocks process_refund when the amount exceeds $500 and redirects to a human escalation workflow

    A PreToolUse hook runs before execution - the refund never processes. The exam specifically warns against using PostToolUse for blocking, because by that point the action has already occurred. Pre-execution interception is the only correct hook direction for policy enforcement.

    You should see: A pre-execution hook that inspects process_refund calls, checks the amount parameter, and blocks the call with a redirect message if the amount exceeds 500. The refund tool never executes for blocked calls.

    Hints
    1. Which hook direction blocks actions before they happen? What happens to the tool call when it is blocked?
    2. Use a PreToolUse hook (pre-execution). Check if the tool name is process_refund and the amount exceeds 500. If so, return a block result instead of allowing execution.
    3. const toolCallInterceptor = (toolName: string, input: Record<string, unknown>) => {
        if (toolName === "process_refund" && (input.amount as number) > 500) {
          return {
            blocked: true,
            message: "Refund exceeds $500 threshold. Redirecting to human escalation queue. Reference: ESC-" + Date.now()
          };
        }
        return { blocked: false };
      };
  5. Add a second PreToolUse hook that blocks transfer_funds until aml_check has returned a pass result in the current session

    This is the AML compliance scenario from the exam. Prompt instructions achieve 95% compliance, but regulatory requirements demand 100%. The hook provides deterministic enforcement that no prompt can match - a single missed AML check can result in legal penalties.

    You should see: A pre-execution hook that checks session state for a completed AML check before allowing transfer_funds to execute. Without a prior passing aml_check, the transfer is blocked with a descriptive error.

    Hints
    1. How do you track whether aml_check has passed? You need session-scoped state, similar to the prerequisite gate pattern.
    2. Maintain a session state variable that records AML check results. The interception hook for transfer_funds checks this state. If no passing AML check exists, block the transfer.
    3. const amlState = { passed: false };
      
      const amlInterceptor = (toolName: string, input: Record<string, unknown>) => {
        if (toolName === "aml_check") {
          // Allow execution; PostToolUse hook records the result
          return { blocked: false };
        }
        if (toolName === "transfer_funds" && !amlState.passed) {
          return {
            blocked: true,
            message: "COMPLIANCE BLOCK: International transfer requires AML verification. Run aml_check first."
          };
        }
        return { blocked: false };
      };
      // In PostToolUse hook for aml_check:
      // if (result.status === "pass") amlState.passed = true;
  6. Test both hooks by attempting to trigger the blocked operations and verify they are prevented before execution

    Testing confirms that the hooks provide deterministic enforcement. The key verification is that blocked tools never execute - the hook prevents the call, not just logs a warning after the fact.

    You should see: Both blocked operations return interception messages without the underlying tool executing. After satisfying prerequisites (completing AML check, reducing refund amount), the operations succeed.

    Hints
    1. Test the negative case (blocked) and the positive case (allowed) for each hook. What should you check to confirm the tool never executed?
    2. For each hook, first attempt the operation without satisfying the prerequisite. Verify it is blocked. Then satisfy the prerequisite and retry. Verify it succeeds. Check that no side effects occurred during the blocked attempt.
    3. // Test refund threshold
      let result = await callTool("process_refund", { customer_id: "C-001", amount: 750 });
      console.log("Blocked high refund:", result.blocked === true);
      
      result = await callTool("process_refund", { customer_id: "C-001", amount: 200 });
      console.log("Allowed low refund:", result.blocked === false);
      
      // Test AML prerequisite
      result = await callTool("transfer_funds", { to: "IBAN-123", amount: 10000 });
      console.log("Blocked without AML:", result.blocked === true);
      
      await callTool("aml_check", { customer_id: "C-001" });
      result = await callTool("transfer_funds", { to: "IBAN-123", amount: 10000 });
      console.log("Allowed after AML:", result.blocked === false);

Sources