Task 5 · 8 tasks
Hooks and interrupts
Pause the agent for human approval before any destructive SQL reaches the database.
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.
Build it
Write an approval hook
Challenge
Finish
ApprovalHook: before anyquery_dbcall whose SQL contains DELETE, UPDATE, INSERT, DROP, ALTER or TRUNCATE, pause and ask a human. Anything butycancels the call.Work in
phase1/starter/t5_hooks_interrupts.py(look forTODO; an unfinished one prints[starter] TODO …) and run it withuv run bootcamp.py phase1 t5 --starter. The reference solution isphase1/hooks.py;uv run bootcamp.py phase1 t5runs it.Hint 1
Hooks are typed lifecycle callbacks. You want
BeforeToolCallEvent, registered from aHookProvider. The event caninterrupt()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 fromevent.tool_use["input"]["query"]. The interrupt's return value is the human's answer.pseudocoderead onlydef 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.pyDESTRUCTIVE_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"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 ofresult.interrupts, each with anid,nameandreason.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.pyresult = 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)Try it, safely
terminal · your starter fileuv run bootcamp.py phase1 t5 --starter "Delete employee with ID 5"terminal · reference solutionuv 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.dband re-runuv run bootcamp.py phase1 t0for a fresh one.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.