Welcome to this course on exploring advanced features of the Claude Agent SDK in Python! In this first lesson, we're diving into one of the most powerful capabilities the SDK offers: hooks. When you build AI agents that can execute commands, read files, or interact with systems, safety becomes critical. Hooks are callback functions that let you intercept and control your agent's behavior at key moments during execution, acting as checkpoints where you can inspect what's about to happen and decide whether to allow it.
In this lesson, you'll learn how to attach hooks to your agent's lifecycle events, inspect incoming user prompts, and enforce safety rules before tools execute. By the end, you'll have working code that blocks dangerous requests and prevents risky commands from running.
The Claude Agent SDK provides six different types of hooks that trigger at various points in your agent's execution lifecycle:
- UserPromptSubmit: Fires when a user submits a prompt, before the agent processes it, giving you a first line of defense for analyzing user intent.
- PreToolUse: Triggers right before the agent executes any tool, allowing you to validate whether a specific action should be allowed.
- PostToolUse: Runs immediately after a tool finishes executing, which is useful for logging and auditing.
- Stop: Fires when the agent completes its execution cycle, making it perfect for cleanup operations.
- PreCompact: Triggers when conversation history is about to be compressed due to token limits.
- SubagentStop: Activates when a delegated sub-agent completes its task.
In this lesson, we'll focus on implementing two of the most commonly used hooks: UserPromptSubmit for intent validation and PreToolUse for action control, which form the foundation of a robust safety system.
Every hook function follows the same signature pattern. It's an asynchronous function that receives three parameters and must return a dictionary that controls what happens next.
The hook function signature has three key parameters:
input_data: A dictionary containing event-specific information that varies by hook type. ForUserPromptSubmithooks, it contains the"prompt"key; forPreToolUsehooks, it contains"tool_name"and"tool_input".tool_use_id: An optional identifier for the tool invocation, useful for tracking specific executions.context: AHookContextobject with execution metadata (it’s currently minimal in Python—think “extra runtime context like cancellation”—and may expand in future SDK versions).
The function must return a dictionary to control what happens next. Returning an empty dictionary signals that everything is fine and the agent should proceed normally.
Important: the shape of the returned dictionary is hook-specific: UserPromptSubmit returns a prompt-level allow/block decision (e.g., ), while returns a tool-permission decision (under ) because it’s controlling a specific tool invocation right before execution.
Our first hook will inspect user prompts before the agent processes them. For the UserPromptSubmit hook, the input_data dictionary contains a "prompt" key with the user's input text.
We extract the prompt using .get("prompt", "") with a default empty string for safety, then convert it to lowercase for case-insensitive matching. The print statement helps us see what the hook is inspecting during execution. Next, we'll add the logic to check for dangerous keywords.
Now we'll add the keyword checking logic that identifies and blocks potentially dangerous requests.
We define a list of blocked keywords that represent potentially dangerous or malicious intent, including terms like "hack" and "exploit", as well as prompt injection attempts. When a match is found, we return a dictionary with "decision" set to "block" to stop processing, and "systemMessage" provides a user-facing explanation. If no blocked keywords are found, we return an empty dictionary to allow the prompt through.
Our second hook operates at a different stage: right before a tool executes. We'll start by checking if the tool being invoked is Bash, since we only want to inspect Bash commands.
For PreToolUse hooks, the input_data dictionary contains "tool_name" and "tool_input" keys. We first check if the tool is "Bash" and return an empty dictionary immediately if it's not, making our hook efficient. If it is a Bash command, we extract the actual command string from the nested tool_input dictionary. Next, we'll add the pattern matching logic.
Now we'll add the logic to identify and block dangerous command patterns.
We define a list of dangerous patterns, including "rm -rf" (recursive force delete), "sudo" (privilege escalation), and even fork bombs. When we find a dangerous pattern, we return a specific structure required by the hook: a dictionary containing , set to , and explaining why. If no dangerous patterns are found, we return an empty dictionary to allow the command.
Now that we've built both hooks, we need to register them with our agent through the ClaudeAgentOptions configuration object. The hooks parameter accepts a dictionary where keys are hook type names and values are lists of HookMatcher objects that specify which tools the hook applies to and which hook functions to execute.
Note on matcher: matcher="*" means “apply to all tools”; you can also match a specific tool name like matcher="Bash" (and depending on SDK support, simple patterns), but we’ll keep "*" here and filter inside the hook for clarity.
The HookMatcher object takes two parameters: matcher specifies which tools this hook applies to (using for all tools or a specific tool name like ), and is a list of callback functions that execute when the hook triggers. You can register multiple hooks for the same event, and they'll execute in the order you list them. Notice that we pass the hook functions we defined earlier ( and ) directly to the parameter — this connects our safety logic to the agent's execution lifecycle.
Now let's put our hooks to work by creating a main function that tests different scenarios. This function sets up the agent with our two safety hooks registered and runs through a series of test prompts to demonstrate how the hooks work at each level.
The main function creates a ClaudeAgentOptions object with both hooks registered, specifying that the agent can use the Bash tool with a maximum of 5 turns. It then creates a ClaudeSDKClient and tests three different prompts: one with a blocked keyword, one with a dangerous command, and one that's completely safe. The loop processes each prompt sequentially, allowing us to see how our hooks intercept and control the agent's behavior at different stages. Let's see what happens when we run this code.
When we run the code with our first test prompt, the intent guardrail immediately blocks it.
The hook detects the word "hack" and prevents the agent from even seeing this request. Notice that there's no agent response because the prompt never reached the agent — our first layer of defense worked perfectly. Now let's see what happens with the second prompt.
The second prompt passes the intent guardrail but triggers our Bash safety guardrail when the agent tries to execute a dangerous command.
The prompt passes the intent guardrail because it doesn't contain blocked keywords, and the agent receives the request. However, when the agent tries to run rm -rf /tmp/test, our Bash safety guardrail intercepts it and denies the tool execution. The agent gracefully handles the denial by explaining the safety restriction and suggesting alternatives — this demonstrates our second layer of defense in action. Let's see what happens with a safe request.
The third prompt demonstrates that our hooks allow legitimate operations to proceed normally.
This prompt is completely safe — it passes the intent guardrail, and when the agent executes ls -la, the Bash safety guardrail inspects it, finds no dangerous patterns, and allows it to run. The agent successfully lists the directory contents and presents them in a formatted table, showing that our hooks provide security without blocking legitimate operations.
You've now learned how to use hooks to gain visibility and control over your Claude agent's execution! You implemented two critical hook types — UserPromptSubmit for validating user intent and PreToolUse for controlling tool executions — creating a defense-in-depth approach in which the intent guardrail catches problems at the prompt level while the tool safety guardrail provides a second layer of protection at the execution level.
In the upcoming practice exercises, you'll extend these concepts by implementing additional safety rules, creating logging hooks, and building more sophisticated guardrails. Remember that on CodeSignal, all necessary libraries are pre-installed, so you can focus on learning the concepts without worrying about environment setup.
