Task 5 · 8 tasks

Hooks and interrupts

Pause the agent for human approval before any destructive SQL reaches the database.

25 minMedium
Alice’s ask

The “Rogue Agent” problem

During testing the agent cheerfully ran a DELETE. Alice wants a human in the loop: reads are fine, anything that changes data needs explicit approval.

You

Build it

  1. Write an approval hook

    Challenge

    Finish ApprovalHook: before any query_db call whose SQL contains DELETE, UPDATE, INSERT, DROP, ALTER or TRUNCATE, pause and ask a human. Anything but y cancels the call.

    Work in phase1/starter/t5_hooks_interrupts.py (look for TODO; an unfinished one prints [starter] TODO …) and run it with uv run bootcamp.py phase1 t5 --starter. The reference solution is phase1/hooks.py; uv run bootcamp.py phase1 t5 runs it.

    Hint 1

    Hooks are typed lifecycle callbacks. You want BeforeToolCallEvent, registered from a HookProvider. The event can interrupt() the agent and can cancel the tool. See Strands: hooks and interrupts.

    Hint 2

    Read the tool name from event.tool_use["name"] and the SQL from event.tool_use["input"]["query"]. The interrupt's return value is the human's answer.

    pseudocoderead only
    def approve(self, event):
        if <not query_db>: return
        if <destructive>:
            answer = event.interrupt("approval-required", reason={"query": query})
            if answer != "y": event.cancel_tool = "..."
    Solution
    phase1/hooks.py
    DESTRUCTIVE_KEYWORDS = ("DELETE", "UPDATE", "INSERT", "DROP", "ALTER", "TRUNCATE")
    
    
    class ApprovalHook(HookProvider):
        def register_hooks(self, registry: HookRegistry, **kwargs) -> None:
            registry.add_callback(BeforeToolCallEvent, self.approve)
    
        def approve(self, event: BeforeToolCallEvent) -> None:
            if event.tool_use.get("name") != "query_db":
                return
            query = event.tool_use["input"].get("query", "")
            if any(word in query.upper() for word in DESTRUCTIVE_KEYWORDS):
                approval = event.interrupt("approval-required", reason={"query": query})
                if str(approval).strip().lower() != "y":
                    event.cancel_tool = "User denied permission to run this query"
  2. Answer the interrupt

    Challenge

    In the same starter file, when the agent stops for an interrupt, ask the human and resume the agent with the answers (make_responder).

    Hint 1

    An interrupted run returns with result.stop_reason == "interrupt" and a list of result.interrupts, each with an id, name and reason.

    Hint 2

    Resume by calling the agent again with a list of {"interruptResponse": {"interruptId": ..., "response": ...}}. Loop: one answer may trigger another interrupt.

    Solution
    phase1/t5_hooks_interrupts.py
    result = agent(prompt)
    while result.stop_reason == "interrupt":
        responses = [
            {"interruptResponse": {"interruptId": i.id, "response": ask_approval(i.reason["query"])}}
            for i in result.interrupts
            if i.name == "approval-required"
        ]
        result = agent(responses)
  3. Try it, safely

    terminal · your starter file
    uv run bootcamp.py phase1 t5 --starter "Delete employee with ID 5"
    terminal · reference solution
    uv run bootcamp.py phase1 t5 "Delete employee with ID 5"

    One-shot runs auto-deny destructive queries; leave out the prompt to be asked y/N.

    The database is your own copy, so approving a delete only affects you. Delete phase1/datastream_corp.db and re-run uv run bootcamp.py phase1 t0 for a fresh one.

  4. Experiments

    • Keyword matching is crude. Can you phrase a request that changes data but slips past the list? What would a sturdier check look like?
    • Why does the deployed agent in Phase 2 block instead of asking? Who would answer?

Check your work

Phase 1 has no automated test: you check it by running the task and looking for the result below.

“Delete employee with ID 5” pauses and asks for approval. Answering y runs it; anything else cancels it and the agent reports that permission was denied. Read-only questions never prompt.

Under the hood

Hooks are typed lifecycle callbacks (before/after model call, before/after tool call, and more). event.interrupt() suspends the agent loop and surfaces a named interrupt to the caller; the response you send back becomes the return value inside the hook. Setting event.cancel_tool skips the tool and returns your message to the model as the tool result.