Lesson 5: Control Your Agent With Hooks
Code for this lesson can be found here.
The Need for Reliability Over Probability
Section titled “The Need for Reliability Over Probability”We’ve built an agent with tools, but when the agent decides to call a tool, it just calls it. There’s nothing you can do about it. Models are getting smarter all the time, and they’re very capable. But capability isn’t the same as reliability. You can’t guarantee the model won’t do something destructive, call the same tool in an infinite loop, or pass bad inputs to a sensitive API without guardrails in place.
Designing an agent harness thoughtfully allows you to tackle these problems. And one of the main ways to mitigate these risks is to inject deterministic code into the agent lifecycle at specific points through hooks. They give you a place where you can enforce the things that can’t be left to probability, like approval gates, rate limits, input validation, audit logging, and safety checks.
Hooks are checkpoints in the agent lifecycle. They exist for the same reason web frameworks have request interception layers. You don’t just trust every request handler to check off or log correctly, so you put that in a layer that is guaranteed and is applied reliably. Hooks are that layer, but for agents.
You’ve actually already seen hooks in action in the first lesson when the coding agent paused and asked for approval before running certain operations. That approval workflow was implemented through hooks.
How Hooks Work Under the Hood
Section titled “How Hooks Work Under the Hood”The way hooks work is pretty straightforward: You write a callback function, register it for a lifecycle event, and your function gets called whenever the event fires. Multiple hooks can listen to the same event, so you can stack behaviors. You can have one hook for logging, another for validation, and another for approval gates without them stepping on each other or interfering.
Interrupts are one of the most common reasons to use hooks. Let’s say you’re building an agent that runs locally on someone’s machine, and one of the things it can do is manipulate files. What you might want is a hook that pauses the agent before any file deletion happens and asks the user for approval. The user can see what’s about to be deleted, approve or deny the action, and the agent either continues or cancels based on that response.
Coding an Interrupt-Gated File Deletion
Section titled “Coding an Interrupt-Gated File Deletion”Let’s look at some code to do this. For the agent itself, we have several tools for file
management, including a delete file tool. Then we have a hook defined here. This is a hook
called DeleteApprovalHook, which subclasses HookProvider from Strands. HookProvider
is a base class that gives you a structured way to register hook callbacks.
Inside register_hooks, we wire up our callback to the specific lifecycle event we care
about. In this case, we register check_delete to fire on every before_tool_call event.
Inside check_delete, the first thing we do is inspect the tool name. If the tool isn’t
delete_file, we immediately return and allow execution to continue normally. But if the
tool is attempting a delete operation, we call event.interrupt(). This pauses the agent
loop and returns control back to our application.
Your application can then prompt the user for approval before resuming with the agent
operations. If the user declines the action, we set event.cancel_tool to a message
explaining why the tool execution was blocked. That cancellation message gets passed back
to the agent loop as a tool result, and the model adapts its behavior accordingly.
Then, during agent initiation, we pass the hook into the agent using
hooks=[DeleteApprovalHook()], and the agent harness handles the interrupt lifecycle,
checking if the agent paused, prompting the user, and feeding the response back to the
agent until the request completes.
Let’s run it. First, I’ll ask the agent to create a file called data.txt. The agent uses
the file tools normally. Then, I’ll ask the agent to delete the data.txt file. A hook
fires before the delete tool executes and pauses the loop. The application asks for
approval, and I’ll type n to deny the operation. The hook cancels the tool call, passes
the cancellation back into the loop, and the agent continues without deleting the file.
Preventing Infinite Loops and Runaway Costs
Section titled “Preventing Infinite Loops and Runaway Costs”Now let’s look at another kind of hook using the same overall structure. We subclass
HookProvider, register a before_tool_call event, and this time, instead of
interrupting for human approval, we track how many times each tool has been called during
the request.
The LimitToolCounts hook accepts a max_calls threshold and stores counts for each
tool. On every tool call, it increments the count and checks whether the tool exceeded the
allowed limit. If the limit is exceeded, it sets event.cancel_tool to a message
instructing the model to stop calling that tool. The model sees that cancellation as the
tool result and adjusts its behavior accordingly.
This is useful because agents can occasionally get stuck in loops, repeatedly calling the same failing tool. A lightweight, deterministic safeguard like this can prevent runaway costs and unstable behavior. This is one of the main reasons why tuning your harness is important for cost control and reliability.
What’s Next?
Section titled “What’s Next?”These are simple examples, but hooks are one of the foundational mechanisms that make production agents safe, observable, and controllable. They’re also the foundation for plugins, which package hooks, tools, and context management into reusable components, which we’ll be looking at next.
Starting in the next lesson, we’ll begin building a customer service agent to show that these same patterns and ideas apply across different use cases. That agent will become one of the main running examples for the rest of the course.
Learn more: Hooks