Welcome to this course on exploring advanced features of the Claude Agent SDK in TypeScript! 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 is about to happen and decide whether to allow it.
The TypeScript SDK provides a HookCallback type for defining these functions, and you register them through the Options object when calling query(). The query() function returns an AsyncIterable<SDKMessage> that yields messages as the agent processes your request — and your hooks execute at specific points in this flow, giving you fine-grained control.
In this lesson, you will 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 will have working code that blocks dangerous requests and prevents risky commands from running.
The Claude Agent SDK provides multiple hook types that trigger at various points in your agent's execution lifecycle. In this lesson, we'll focus on implementing the two most commonly used hooks that form the foundation of a robust safety system:
- 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.
These two hooks work together to create a defense-in-depth approach: the UserPromptSubmit hook catches problems at the prompt level (blocking malicious intent before it reaches the agent), while the PreToolUse hook provides a second layer of protection at the execution level (preventing dangerous commands from running even if they slip through intent validation).
Every hook function follows the same signature pattern defined by the HookCallback type. It is an asynchronous function that receives three parameters and must return an object that controls what happens next.
The hook function signature has three key parameters:
input: An object containing event-specific information that varies by hook type. You will need to cast this to the appropriate type to access its properties.toolUseID: An optional identifier for the tool invocation, useful for tracking specific executions.{ signal }: A destructured object containing execution metadata (primarily for cancellation support and future extensibility).
Returning an empty object signals that everything is correct and that the agent should proceed normally. To block or modify behavior, you must return a hook-specific response.
While the generic signature is always the same, the shape of the input and the required return object depend on which hook you are using. You must cast the input to access properties and follow the specific return structure for that event.
Important: The SDK uses strict literal types for decisions. Using as const type assertions ensures TypeScript treats strings as specific literal types (like "block" or "deny") rather than general strings.
For UserPromptSubmit, you cast input to UserPromptSubmitHookInput and return a prompt-level decision:
For PreToolUse, you cast input to PreToolUseHookInput and return a tool-permission decision under hookSpecificOutput:
Our first hook will inspect user prompts before the agent processes them. Let's start with the basic structure that extracts and logs the user's prompt.
We cast input to UserPromptSubmitHookInput to access the prompt property safely, then convert it to lowercase for case-insensitive matching. The console.log helps us see what the hook is inspecting during execution. This basic structure follows the "cast → normalize → return" pattern that all hooks use.
Now, we will add the keyword-checking logic between the console.log and the return {} statement:
This completes our intent guardrail by adding the "scan keywords" step to our pattern. We define an array of blockedKeywords representing dangerous or malicious intent. When a match is found, we return an object with decision set to "block" as const and a systemMessage providing a user-facing explanation. The as const assertion ensures TypeScript treats "block" as the specific literal type required by the SDK. If no blocked keywords are found, execution continues to the final that allows the prompt through.
Our second hook operates at a different stage: right before a tool executes. Let's start with the structure that extracts and validates the tool information.
For PreToolUse hooks, we cast input to PreToolUseHookInput to access the tool_name and tool_input properties. We first check whether the tool is "Bash" and return early if it is not, making our hook efficient. If it is a Bash command, we extract the actual command string using safe type casting and the nullish coalescing operator. This follows the same "cast → normalize → return" pattern as our intent guardrail.
Now, let's add the pattern-checking logic between the console.log and the final return {} statement:
This completes our Bash safety guardrail by adding the "scan patterns" step. We define an array of dangerousPatterns including destructive commands like "rm -rf" and privilege escalation attempts. When we find a dangerous pattern, we return the specific structure required by PreToolUse hooks: a hookSpecificOutput object containing hookEventName, set to , and . All literal values use assertions to satisfy the SDK's type requirements. If no dangerous patterns are found, execution continues to the final that allows the command.
Now that we have built both hooks, we need to register them with our agent through the Options configuration object. The hooks property accepts an object in which the keys are hook type names and the values are arrays of objects specifying which tools the hook applies to (via matcher) and which hook functions to execute (via the hooks array).
Each hook registration object contains two properties: matcher, which specifies the tools to which this hook applies (using "*" for all tools or a specific tool name like "Bash"), and hooks, which is an array of callback functions that execute when the hook triggers. You can register multiple hooks for the same event, and they will execute in the order in which you list them. Notice that we pass the hook functions we defined earlier (intentGuardrail and ) directly to the arrays — 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 an Options object with both hooks registered, specifying that the agent can use the Bash tool with a maximum of 5 turns. It then defines three test prompts: one with a blocked keyword, one with a dangerous command, and one that is completely safe. The loop processes each prompt sequentially by calling query({ prompt, options }), which returns an AsyncIterable<SDKMessage>. We pass this iterable to displayResponse() from our utils.ts file, which consumes the messages and prints them in a formatted way. 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 is no 💬 Claude 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 does not contain blocked keywords, and the agent receives the request. The agent plans to use the Bash tool, which triggers the 🔧 [Tool: Bash] indicator from displayResponse(). However, when the agent tries to run rm -rf /tmp/test, our Bash safety guardrail intercepts it and denies the tool execution.
Important: Notice that the agent suggests alternatives it considers "safer," but these are still destructive operations — they are less forceful, not truly safe. Commands like find ... -delete or rm -r can still delete files irreversibly. In production systems, you would implement more robust guardrails such as:
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 🔧 [Tool: Bash] marker appears when the tool is invoked, and the agent successfully lists the directory contents, 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.
You learned the consistent "cast → normalize → scan → return" pattern that all hooks follow: cast the input parameter to the appropriate type (UserPromptSubmitHookInput or PreToolUseHookInput), normalize and extract the data you need to inspect, scan for problems using your safety rules, and return either a blocking decision (using as const assertions for literal types) or an empty object to proceed. You saw how to register these hooks in the Options object and pass them to query(), which returns an AsyncIterable<SDKMessage> that your displayResponse() utility consumes to show formatted output.
In the upcoming practice exercises, you will extend these concepts by implementing additional safety rules, creating logging hooks, and building more sophisticated guardrails.
