Introduction

Welcome back to Agent SDK! In the first two units, we explored the Agent SDK's foundations and mastered the Python SDK's built-in capabilities. We learned how to configure agents with different permission modes, restrict tool access for safety, and build robust automation using the standard toolkit.

In this third unit, we take a significant step forward by creating our own custom tools and implementing hooks to control agent behavior. While built-in tools like Read, Write, and Bash cover many scenarios, real-world applications often require specialized capabilities. For instance, we might need a calculator that logs operations, a database query tool, or custom validation logic.

We will learn how to build these tools directly in Python and integrate them seamlessly with the SDK. We will also explore hooks, which are callback functions that run at specific points in the agent lifecycle, allowing us to add safety checks, logging, and custom business logic without modifying Claude's core behavior.

Understanding Custom Tools

Before diving into the code, let us understand what custom tools actually are within the SDK ecosystem. When we use built-in tools like Read or WebFetch, Claude invokes them through a structured protocol called MCP (Model Context Protocol). Each tool has a name, description, input schema, and implementation.

Custom tools follow the same pattern, but instead of being built into the SDK, we define them ourselves using Python functions. The SDK provides a @tool decorator that transforms regular async functions into MCP-compatible tools that Claude can discover and invoke. These tools run in the same Python process as our agent, making them efficient and easy to debug.

The workflow is straightforward: we write an function that performs a task, decorate it with to specify its and , and then package it into an . The agent can then use this tool just like any built-in capability; the key difference is that we control exactly how it functions.

The @tool Decorator

Let us create our first custom tool using the @tool decorator. Here is a simple addition tool:

The @tool decorator takes three arguments: the tool's name (add), a description that helps Claude understand when to use it, and an input schema defining the expected parameters. Our schema indicates that this tool requires two floats: a and b.

The decorated function receives args, which is a dictionary containing the input values. We perform the calculation and return a structured response with a content array. This format matches the MCP protocol, allowing Claude to understand and display the result properly.

Creating SDK MCP Servers

Individual tools must be packaged into an MCP server before the agent can access them. The SDK provides create_sdk_mcp_server for this purpose:

We create an MCP server named calculator containing both our addition and multiplication tools. The server acts as a container, grouping related functionality together. Notice that the tools parameter takes a list; as many tools as needed can be included in a single server.

Using Custom Tools

We can now use our calculator tools in an agent. Here is the complete setup:

We pass our calculator server to mcp_servers with the key calc. This key becomes part of the tool names in allowed_tools: when Claude accesses these tools, they will be prefixed with the pattern mcp__{key}__{tool_name}, resulting in mcp__calc__add and mcp__calc__multiply. This namespacing prevents conflicts when multiple servers provide similarly named tools.

When Claude processes the prompt, it recognizes that addition is required, invokes mcp__calc__add with the arguments {"a": 123, "b": 456}, and receives the result.

The agent successfully used our custom tool to compute the answer. This same pattern works for any custom functionality we require, such as , , specialized calculations, or domain-specific operations.

Benefits Over External MCP Servers

One might wonder why we would create SDK-based MCP servers when external MCP servers (separate processes that communicate via stdio) already exist. SDK-based servers offer several compelling advantages for Python-based automation.

No subprocess management means everything runs within the same process. We do not need to spawn separate Claude CLI instances or manage inter-process communication. This simplifies deployment and reduces the potential for errors.

Better performance results from eliminating IPC overhead. Function calls occur directly in Python without serialization, socket communication, or process boundaries. For tools invoked frequently, this improvement is significant.

Simpler deployment is a major benefit: our entire agent is a single Python script. We do not need to configure external server paths, manage multiple processes, or worry about version compatibility between the server and the client.

Easier debugging allows us to use standard Python debugging tools. We can set breakpoints inside our tool implementations, inspect variables, and trace execution flow without dealing with the complexity of cross-process debugging.

Understanding Hooks

Hooks represent a different kind of customization: instead of adding new capabilities, they let us intercept and influence the agent's behavior at specific points in its lifecycle. You have already encountered hooks in the previous course when working with the Claude CLI, where they were bash scripts that executed at specific trigger points.

In the Python SDK, hooks work on the same principle but with an important difference: they are Python functions rather than bash scripts. They execute in the same process as our agent, with full access to Python's capabilities. Think of hooks as callback functions that run before or after certain events.

The SDK supports several hook types, with PreToolUse and PostToolUse being the most common. PreToolUse hooks run before a tool is invoked, allowing us to validate inputs, block dangerous operations, or modify parameters. PostToolUse hooks run after tool execution, which is ideal for logging, auditing, or triggering follow-up actions.

Hooks receive context about which tool is being used and the data involved, then return a dictionary that can influence how the agent proceeds.

PreToolUse Hook for Safety

Let us implement a safety hook that blocks dangerous bash commands. This hook inspects Bash tool invocations before they execute:

The hook receives input_data containing information about the tool invocation. We check if it is a Bash command, extract the command string, and scan for dangerous patterns. If we find a match, we return a dictionary with permissionDecision: "deny", which prevents the tool from executing.

Returning an empty dictionary {} indicates the hook has no objection, and the tool proceeds normally. This pattern gives us fine-grained control over which operations are permitted.

PostToolUse Hook for Logging

Logging hooks track tool usage after execution completes. Here is a simple audit logger:

This hook runs after any tool completes, printing an audit message. Unlike the safety hook, this one does not make decisions; it simply observes and records. The empty dictionary return value indicates that we are not modifying the agent's behavior, just adding logging.

We can make logging more sophisticated by writing to files, sending data to monitoring systems, or accumulating statistics. The key is that PostToolUse hooks provide visibility into every tool invocation without modifying the tools themselves.

Testing Hook Behavior

Now, let us configure an agent with both hooks and observe them in action. We use a HookMatcher to define when our hooks apply:

We configure hooks using the hooks dictionary, mapping hook types to HookMatcher objects. The matcher parameter filters when hooks apply; Bash indicates they apply only to Bash tool invocations. We can also omit the matcher (as in PostToolUse) to run the hook on all tools.

When Claude attempts to run the dangerous command, our PreToolUse hook blocks it. The PostToolUse hook still runs because it tracks attempts rather than just successes. Let us test a safe command:

Complete Example: File Monitor Setup

Let us build a complete practical example: a file monitor that logs all modifications with timestamps. This combines custom hooks with file operation tools to create an audit trail.

First, we implement the logging hook:

This hook specifically watches Write and Edit operations. When either tool is used, we extract the file_path, generate a timestamp, and append an entry to audit.log. This creates a persistent record of all file modifications the agent makes.

Notice that we use standard Python file operations inside the hook. Hooks have full access to the Python environment; they are not sandboxed or restricted.

File Monitor Configuration

With the hook implemented, we configure the agent to use it:

We use acceptEdits permission mode so that file operations proceed without manual approval, making the agent fully automated. The matcher="Write|Edit" uses regex syntax to apply the hook only to file modification tools, excluding Read operations.

This configuration creates an agent that can freely modify files while maintaining a complete audit trail. The hook runs after each modification, ensuring we capture every change.

Running the File Monitor

Now we put the agent to work with several file operations:

We make two queries: the first creates a file (hello.py), and the second edits it. We process messages silently (using pass) because we are focused on the audit trail rather than the agent's responses. After the operations complete, users can examine audit.log to see what occurred.

Each file operation triggered our hook, creating console output and appending to the audit log. The audit.log file now contains timestamped entries showing exactly when each modification occurred and which tool was used. This pattern is valuable for compliance, debugging, or understanding how an automated agent modified a codebase over time.

Conclusion and Next Steps

We have explored two powerful customization mechanisms in the Agent SDK: custom tools and hooks. We learned how the @tool decorator transforms Python functions into MCP-compatible tools, how to package them with create_sdk_mcp_server, and why SDK-based servers offer advantages over external processes. We also discovered how hooks act as callback functions that intercept agent behavior, implementing safety checks with PreToolUse and logging with PostToolUse.

The file monitor example demonstrated these concepts working together: custom logic integrated seamlessly into the agent's workflow, adding capabilities such as audit logging without modifying core tools. We saw how HookMatcher filters when hooks apply, how to return permission decisions to block operations, and how hooks access the full Python environment.

The key insight is that the SDK is not just a fixed toolkit; it is an extensible platform. We can add precisely the capabilities we need through and enforce exactly the policies we want through . This flexibility makes the suitable for production systems requiring specialized functionality, compliance controls, or integration with existing infrastructure. Now it is time to practice: the upcoming exercises will challenge you to build your own and implement sophisticated , preparing you for real-world agent development!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal