Skip to content

Lesson 5: Control Your Agent With Hooks

Play

Watch on YouTube

Code for this lesson: samples/05-hooks

Hooks inject code at lifecycle events (before/after tool calls, before/after the agent loop) without changing the agent’s logic. Unlike tools, which the model decides to use, hooks fire automatically every time regardless of what the model reasons. That’s the key distinction: a prompt is a request, a hook is a guarantee.

A runaway loop could call the same tool dozens of times. A model might attempt a destructive operation without asking. Prompting asks the model to behave. Hooks guarantee it.

  • Subclass HookProvider to define hooks
  • Register callbacks for lifecycle events in register_hooks()
  • Multiple hooks can listen to the same event (stackable)
  • event.interrupt() pauses the loop for human input
  • event.cancel_tool blocks tool execution and feeds a message back to the model explaining why
from strands import Agent, tool
from strands.hooks import BeforeToolCallEvent, HookProvider, HookRegistry
class DeleteApprovalHook(HookProvider):
"""Intercepts delete operations for human approval."""
def register_hooks(self, registry: HookRegistry) -> None:
registry.add_callback(BeforeToolCallEvent, self.check_delete)
def check_delete(self, event: BeforeToolCallEvent) -> None:
if event.tool_use["name"] != "delete_file":
return
approval = event.interrupt(
"delete-approval",
reason={"path": event.tool_use["input"]["path"]}
)
if approval.lower() != "y":
event.cancel_tool = "User denied file deletion"
agent = Agent(
tools=[list_files, read_file, write_file, delete_file],
hooks=[DeleteApprovalHook()],
)

📂 approval_interrupt.py

event.interrupt() pauses agent execution and returns control to the caller. That’s what makes approval workflows possible: the agent doesn’t proceed until a human answers.

from strands.hooks import BeforeInvocationEvent, BeforeToolCallEvent, HookProvider, HookRegistry
class LimitToolCounts(HookProvider):
def __init__(self, max_calls: int = 3):
self.max_calls = max_calls
self.counts: dict[str, int] = {}
def register_hooks(self, registry: HookRegistry) -> None:
registry.add_callback(BeforeInvocationEvent, self.reset)
registry.add_callback(BeforeToolCallEvent, self.check)
def reset(self, event: BeforeInvocationEvent) -> None:
self.counts = {}
def check(self, event: BeforeToolCallEvent) -> None:
name = event.tool_use["name"]
self.counts[name] = self.counts.get(name, 0) + 1
if self.counts[name] > self.max_calls:
event.cancel_tool = (
f"'{name}' hit the {self.max_calls}-call limit. "
"Do NOT call this tool again."
)
agent = Agent(tools=[get_weather], hooks=[LimitToolCounts(max_calls=3)])

📂 rate_limiter.py

Notice the counter resets on BeforeInvocationEvent. Rate limits are per request, not for the agent’s lifetime. And because cancel_tool sends a message back to the model, the model learns why the call was blocked and stops retrying.

  • Rate limit tool usage to prevent runaway loops
  • Require human approval before destructive operations
  • Log every tool call for audit trails
  • Validate tool inputs and outputs against business rules
  • Enforce access control on sensitive tools

Hooks don’t touch tools or prompts. They’re a separate, reusable layer you can drop onto any agent.