Claude Tools12 min read

Claude Agent SDK Tutorial: Build Custom AI Agents in Python & TypeScript (2026)

Learn the Claude Agent SDK step by step: install it, run your first agent, add custom tools, control permissions, and ship a production agent in Python or TypeScript.

Claude Agent SDK Tutorial: Build Custom AI Agents in Python & TypeScript

If you've used Claude Code, you've already used the best agent harness Anthropic has built — a loop that reads files, edits code, runs bash commands, and course-corrects when something breaks. The Claude Agent SDK takes that exact harness out of the terminal and puts it in your own application. Instead of prompting a chat window, you write a few lines of Python or TypeScript and get a fully autonomous agent with file access, bash execution, web search, and your own custom tools.

This guide walks through everything you need to go from zero to a working, permission-controlled agent: installation, the core query() function, multi-turn sessions with ClaudeSDKClient, writing custom tools, and the settings that keep an autonomous agent from doing something you didn't ask for.

What Is the Claude Agent SDK (and How Is It Different from the Claude API)?

The raw Claude API gives you a single request/response loop: send messages, get a completion, optionally get a tool-call request back that you have to execute yourself. That's fine for a chatbot. It's a lot of plumbing for an agent that needs to read ten files, run a test suite, and iterate on failures.

The Claude Agent SDK is a higher-level layer built on the same agent loop that powers Claude Code. It ships with:

  • Built-in tools — file read/write/edit, bash execution, web search, and web fetch, ready to use without you writing a single tool handler
  • An agent loop — Claude plans, calls tools, reads results, and keeps going until the task is done or it needs your input
  • Subagents — you can delegate parts of a task to child agents with their own context window, exactly like Claude Code's subagent feature
  • Permission modes — fine-grained control over which tools can run automatically and which need human approval
  • MCP support — connect any Model Context Protocol server, or bundle your own tools as an in-process MCP server
  • Session persistence — resume a conversation across process restarts

In short: the Claude API answers questions. The Claude Agent SDK gets things done, autonomously, inside guardrails you define.

Claude API vs. Claude Agent SDK: When to Use Which

Raw Claude APIClaude Agent SDK
Tool executionYou implement and run every tool handler yourselfFile, bash, web search/fetch tools built in
Multi-step tasksYou write the loop: call → parse tool request → execute → send result backLoop is managed for you until the task completes
Best forSingle-turn completions, classification, simple RAGCoding agents, ops automation, research agents, anything multi-step
Setup effortLow for simple calls, high once you add tool orchestrationHigher upfront (permissions, tool config), low ongoing
Custom business logicFully manual@tool decorator + in-process MCP server

If your use case is "send text, get text back," stay on the raw API — it's lighter weight. The moment your prompt needs to do things across multiple steps — edit files, hit your database, call three services in sequence — the Agent SDK removes the orchestration code you'd otherwise be writing by hand.

Prerequisites

  • Python 3.10+ or Node.js 18+
  • An Anthropic API key (ANTHROPIC_API_KEY in your environment), or a Claude Pro/Max login for local development
  • Basic familiarity with async/await in your language of choice — every SDK call is asynchronous

Step 1: Install the SDK

Python:

bashpip install claude-agent-sdk

The Claude Code CLI binary is bundled automatically — you don't need to install it separately.

TypeScript / Node.js:

bashnpm install @anthropic-ai/claude-agent-sdk

The TypeScript package bundles a native Claude Code binary for your platform as an optional dependency, so it also works standalone.

Set your API key before running anything:

bashexport ANTHROPIC_API_KEY="sk-ant-..."

Step 2: Your First Agent — the query() Function

The simplest way to run an agent is query(), a single async call that streams messages back as Claude works through the task.

Python:

pythonimport asyncio
from claude_agent_sdk import query, ClaudeAgentOptions

async def main():
    options = ClaudeAgentOptions(
        system_prompt="You are a careful, senior Python developer.",
        permission_mode="acceptEdits",
        cwd="./my-project",
    )
    async for message in query(
        prompt="Add input validation to the signup endpoint and write a test for it.",
        options=options,
    ):
        print(message)

asyncio.run(main())

TypeScript:

typescriptimport { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "Add input validation to the signup endpoint and write a test for it.",
  options: {
    systemPrompt: "You are a careful, senior Python developer.",
    permissionMode: "acceptEdits",
    cwd: "./my-project",
  },
})) {
  console.log(message);
}

Run either one and Claude will open the relevant files, write the validation logic, add a test, and report back — no manual tool-call handling required. Each call to query() starts a fresh session with no memory of prior calls unless you explicitly resume one (see Step 4).

Step 3: Give the Agent Custom Tools

Built-in tools cover files, bash, and the web. For anything domain-specific — hitting your internal API, querying your database, calling a billing system — you write a custom tool with the @tool decorator and bundle it into an in-process MCP server.

Python:

pythonfrom claude_agent_sdk import tool, create_sdk_mcp_server, ClaudeSDKClient, ClaudeAgentOptions

@tool(
    name="get_order_status",
    description="Look up the current status of a customer order by ID",
    input_schema={"order_id": str},
)
async def get_order_status(args):
    order_id = args["order_id"]
    # replace with a real database/API call
    return {"content": [{"type": "text", "text": f"Order {order_id} is 'shipped'."}]}

server = create_sdk_mcp_server(name="orders", tools=[get_order_status])

options = ClaudeAgentOptions(
    mcp_servers={"orders": server},
    allowed_tools=["mcp__orders__get_order_status"],
)

async def main():
    async with ClaudeSDKClient(options=options) as client:
        await client.query("What's the status of order #4471?")
        async for message in client.receive_response():
            print(message)

A few things worth internalizing here:

  • The input_schema dict ({"order_id": str}) is converted to JSON Schema automatically — pass a full JSON Schema dict yourself if you need enums, optional fields, or nested objects
  • Custom tools run in-process, inside your own application — there's no separate server process to deploy or manage
  • Tool names are namespaced as mcp____ when you reference them in allowed_tools

Step 4: Multi-Turn Conversations with ClaudeSDKClient

query() is a one-shot fire-and-forget call. For anything interactive — a support bot, an internal ops assistant, a long-running coding session where you want to inspect Claude's plan before it touches files — use ClaudeSDKClient directly, as shown above. It exposes explicit session control: send a message with .query(), stream the response with .receive_response(), and keep the same client open across many turns so context persists.

If you need to resume a session after a process restart, pass continue_conversation=True or a session ID via resume in ClaudeAgentOptions rather than rebuilding history yourself.

Step 5: Delegate Work with Subagents

Just like Claude Code, the Agent SDK supports subagents — child agents with their own isolated context window that handle a scoped piece of work and report back a summary instead of flooding the parent's context with every intermediate step. This matters once tasks get large: a single agent researching a topic across twenty files or API calls will burn through its context window fast, while a set of subagents can work in parallel and return only the distilled result.

Python:

pythonoptions = ClaudeAgentOptions(
    agents={
        "researcher": {
            "description": "Searches the web and summarizes findings",
            "prompt": "You are a focused research assistant. Return concise, cited summaries.",
            "tools": ["WebSearch", "WebFetch"],
        },
    },
)

Once defined, the parent agent can delegate a subtask to researcher the same way Claude Code delegates to a named subagent — the child runs in its own context, and only its final answer flows back to the parent's conversation.

A good rule of thumb: reach for a subagent when a subtask would otherwise dump a large amount of intermediate content (search results, file contents, logs) into your main conversation that you don't need to keep around after the subtask finishes.

Step 6: Control What the Agent Is Allowed to Do

This is the step most tutorials skip, and it's the one that matters most once you deploy something. An autonomous agent with unrestricted bash and file access is a liability in production. ClaudeAgentOptions gives you several layers of control:

SettingWhat it does
permission_mode"default" asks before risky actions; "acceptEdits" auto-approves file edits; "bypassPermissions" runs everything unattended — use only in sandboxed environments
allowed_toolsExplicit allowlist of tool names the agent can call, including your custom MCP tools
cwdRestricts file operations to a working directory
system_promptSets behavioral guardrails and domain context up front
HooksRegister pre-tool / post-tool callbacks to log, veto, or modify tool calls before they execute

A practical rule for production agents: start with permission_mode="default" and a narrow allowed_tools list, watch the agent's behavior in staging, and only widen permissions once you've seen how it actually behaves on real inputs — not how you assume it will behave.

Adding a Hook for an Audit Trail

Hooks let you intercept every tool call before or after it executes — useful for logging, rate limiting, or vetoing a specific action outright. A minimal pre-tool hook that logs every bash command before it runs:

pythonasync def log_bash_calls(tool_name, tool_input, context):
    if tool_name == "Bash":
        print(f"[audit] about to run: {tool_input.get('command')}")
    return None  # returning None allows the call to proceed

options = ClaudeAgentOptions(
    hooks={"pre_tool_use": [log_bash_calls]},
)

Return a value from the hook (instead of None) to block or modify the tool call — this is the mechanism to use if you need a hard veto on specific commands (e.g., blocking rm -rf) rather than relying on the model to police itself.

Handling Errors and Timeouts

Agent runs are not guaranteed to finish cleanly — a tool call can fail, a task can be genuinely ambiguous, or a run can hit your configured turn limit. Wrap query()/ClaudeSDKClient calls the way you'd wrap any external API call:

pythonimport asyncio

async def run_with_timeout(client, prompt, timeout_s=120):
    try:
        await asyncio.wait_for(client.query(prompt), timeout=timeout_s)
        async for message in client.receive_response():
            yield message
    except asyncio.TimeoutError:
        print("Agent run exceeded timeout — check for a stuck tool loop")

Watch specifically for tool-call loops (the agent repeatedly retrying a failing action) and for max_turns being hit — both ClaudeAgentOptions in Python and TypeScript let you cap turns explicitly so a misbehaving run fails fast instead of burning tokens indefinitely.

Deploying an SDK Agent

A few things change once you move from a local script to a deployed service:

  • Run in a sandbox. If permission_mode="bypassPermissions" or broad bash access is required, isolate the process — a container or VM with no access to production credentials it doesn't need.
  • Separate API keys per environment. Don't share a single ANTHROPIC_API_KEY across dev, staging, and prod; it makes usage attribution and incident response harder than it needs to be.
  • Log tool calls, not just final output. The audit-trail hook above is the minimum bar for a production agent — when something goes wrong, you need to see exactly which tools ran with which arguments, not just the summary Claude produced.
  • Set explicit turn and timeout limits. An unattended agent without a ceiling on turns is a cost and safety risk, especially with bypassPermissions mode.

Common Mistakes to Avoid

  • Skipping allowed_tools entirely. Without an explicit allowlist, the agent has access to every built-in tool, including bash. That's rarely what you want outside a sandbox.
  • Rebuilding conversation history manually. Use resume/continue_conversation instead of concatenating past messages into a new prompt — you'll lose tool-call context doing it by hand.
  • Treating query() and ClaudeSDKClient as interchangeable. query() is for single-shot tasks; reach for ClaudeSDKClient the moment you need more than one turn or fine-grained session control.
  • Forgetting the input schema on custom tools. A vague or missing schema is the single biggest cause of Claude calling your tool with malformed arguments.

Frequently Asked Questions

Is the Claude Agent SDK free to use?

The SDK itself is free and open source. You pay standard Claude API token pricing for the model calls it makes on your behalf — there's no separate SDK fee.

Do I need Claude Code installed to use the SDK?

No. Both the Python and TypeScript packages bundle the Claude Code binary they depend on, so a standalone install works without a separate Claude Code setup.

Can I use the Agent SDK with Claude on Bedrock or Vertex AI?

Yes — the SDK supports the same enterprise deployment paths (Amazon Bedrock, Google Vertex AI) that Claude Code and the Claude API support, configured through the same environment variables.

What's the difference between a custom tool and an MCP server?

A custom tool built with @tool and create_sdk_mcp_server() is an MCP server — it just runs in-process inside your application instead of as a separate process you have to deploy and manage. You can also connect to external, standalone MCP servers the same way Claude Code does.

Key Takeaways

  • The Claude Agent SDK exposes the same agent loop that powers Claude Code — file tools, bash, web search, and subagents — as a Python or TypeScript library you embed in your own app
  • query() handles one-shot tasks; ClaudeSDKClient handles multi-turn, session-controlled conversations
  • Custom tools are just an @tool-decorated function wrapped in create_sdk_mcp_server() — no separate MCP server process required
  • Permission modes, allowed_tools, and hooks are not optional extras — they're what makes an autonomous agent safe to run against real systems

Next Steps

Building agents is exactly the kind of hands-on skill the Claude Certified Architect (CCA-F) exam tests under its Agentic Architecture & Orchestration domain — the single heaviest-weighted section of the exam. If you're prepping for certification, pair this tutorial with hands-on practice: build a small agent with a custom tool this week, then work through AI for Anything's CCA-F practice tests to see how well your intuition matches Anthropic's official exam scenarios.

Ready to Start Practicing?

300+ scenario-based practice questions covering all 5 CCA domains. Detailed explanations for every answer.

Free CCA Study Kit

Get domain cheat sheets, anti-pattern flashcards, and weekly exam tips. No spam, unsubscribe anytime.