The Fast Version

The Claude Agent SDK gives you the same agent runtime that powers Claude Code – built-in file tools, bash execution, web search, and an autonomous agent loop – as a Python library. You don’t implement tool execution yourself. You tell Claude what tools it can use, and the SDK handles the rest.

1
2
pip install claude-agent-sdk
export ANTHROPIC_API_KEY="sk-ant-..."
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions

async def main():
    async for message in query(
        prompt="Find all TODO comments in this project and summarize them",
        options=ClaudeAgentOptions(allowed_tools=["Read", "Glob", "Grep"]),
    ):
        if hasattr(message, "result"):
            print(message.result)

asyncio.run(main())

That’s a working agent. Claude reads your files, searches with regex, and reports back. No tool definitions, no execution logic, no parsing JSON responses. The SDK bundles the Claude Code CLI automatically, so there’s nothing else to install.

How It Differs from the Anthropic Client SDK

If you’ve used the anthropic package before, you’ve written the tool loop yourself: send a message, check if stop_reason == "tool_use", execute the tool, feed the result back, repeat. The Agent SDK eliminates that entire layer.

1
2
3
4
5
6
7
8
9
# With the anthropic client SDK -- you manage the loop
response = client.messages.create(...)
while response.stop_reason == "tool_use":
    result = your_tool_executor(response.tool_use)
    response = client.messages.create(tool_result=result, **params)

# With the Agent SDK -- Claude manages itself
async for message in query(prompt="Fix the bug in auth.py"):
    print(message)

The Agent SDK comes with real tools baked in: Read, Write, Edit, Bash, Glob, Grep, WebSearch, WebFetch, and more. Claude can autonomously read files, run terminal commands, search the web, and edit code. You control what’s allowed through allowed_tools.

Building a Research Agent

Here’s an agent that searches the web, reads pages, and writes a summary file. This is the pattern you’d use for automated research, competitive analysis, or content aggregation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, TextBlock, ResultMessage

async def research(topic: str):
    options = ClaudeAgentOptions(
        system_prompt=(
            "You are a research agent. Search the web for the given topic, "
            "read the most relevant pages, and write a concise summary. "
            "Save the summary to research_output.md."
        ),
        allowed_tools=["WebSearch", "WebFetch", "Write"],
        permission_mode="acceptEdits",
        max_turns=15,
    )

    async for message in query(prompt=f"Research: {topic}", options=options):
        if isinstance(message, AssistantMessage):
            for block in message.content:
                if isinstance(block, TextBlock):
                    print(block.text)
        if isinstance(message, ResultMessage):
            print(f"\nDone. Cost: ${message.total_cost_usd:.4f}")

asyncio.run(research("latest advances in protein folding prediction"))

Key choices here: permission_mode="acceptEdits" auto-approves file writes so the agent doesn’t block waiting for confirmation. max_turns=15 caps iteration so a confused agent doesn’t loop forever. The ResultMessage at the end gives you cost and usage data.

Custom Tools with In-Process MCP Servers

The built-in tools are useful, but real agents need domain-specific capabilities. The SDK lets you define custom tools as Python functions using the @tool decorator. These run as in-process MCP servers, which means no subprocess management and no IPC overhead.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import asyncio
import httpx
from claude_agent_sdk import (
    ClaudeSDKClient,
    ClaudeAgentOptions,
    tool,
    create_sdk_mcp_server,
    AssistantMessage,
    TextBlock,
)

@tool("search_docs", "Search internal documentation by keyword", {"keyword": str})
async def search_docs(args):
    # Replace with your actual search backend
    results = [
        {"title": "Auth Setup Guide", "snippet": "OAuth2 flow for service accounts..."},
        {"title": "Rate Limiting Policy", "snippet": "Default: 1000 req/min per key..."},
    ]
    matches = [r for r in results if args["keyword"].lower() in r["title"].lower()]
    text = "\n".join(f"- {r['title']}: {r['snippet']}" for r in matches) or "No results."
    return {"content": [{"type": "text", "text": text}]}

@tool("create_ticket", "Create a support ticket", {"title": str, "body": str, "priority": str})
async def create_ticket(args):
    # Replace with your ticketing API call
    ticket_id = "TICKET-4821"
    return {"content": [{"type": "text", "text": f"Created {ticket_id}: {args['title']}"}]}

async def main():
    server = create_sdk_mcp_server(
        name="internal-tools",
        version="1.0.0",
        tools=[search_docs, create_ticket],
    )

    options = ClaudeAgentOptions(
        mcp_servers={"internal": server},
        allowed_tools=["mcp__internal__search_docs", "mcp__internal__create_ticket"],
    )

    async with ClaudeSDKClient(options=options) as client:
        await client.query("Search our docs for 'Auth' and create a ticket about updating the OAuth guide")
        async for message in client.receive_response():
            if isinstance(message, AssistantMessage):
                for block in message.content:
                    if isinstance(block, TextBlock):
                        print(block.text)

asyncio.run(main())

The tool naming convention for MCP tools is mcp__<server-name>__<tool-name>. Claude sees the tool descriptions and decides when to call them. The @tool decorator takes a name, description, and input schema – you can use simple Python type mappings like {"keyword": str} or full JSON Schema for complex validation.

Multi-Turn Conversations with ClaudeSDKClient

The query() function creates a fresh session every time. When you need Claude to remember context across multiple exchanges – an interactive assistant, a debugging session, or a multi-step workflow – use ClaudeSDKClient.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import asyncio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, AssistantMessage, TextBlock

async def interactive_debug():
    options = ClaudeAgentOptions(
        allowed_tools=["Read", "Glob", "Grep", "Bash"],
        permission_mode="acceptEdits",
    )

    async with ClaudeSDKClient(options=options) as client:
        # First turn: Claude reads the project
        await client.query("Read the file src/auth.py and identify any bugs")
        async for message in client.receive_response():
            if isinstance(message, AssistantMessage):
                for block in message.content:
                    if isinstance(block, TextBlock):
                        print(block.text)

        # Second turn: Claude remembers what it just read
        await client.query("Now find all files that import from auth.py")
        async for message in client.receive_response():
            if isinstance(message, AssistantMessage):
                for block in message.content:
                    if isinstance(block, TextBlock):
                        print(block.text)

        # Third turn: still has full context
        await client.query("Run the test suite and show me any failures related to auth")
        async for message in client.receive_response():
            if isinstance(message, AssistantMessage):
                for block in message.content:
                    if isinstance(block, TextBlock):
                        print(block.text)

asyncio.run(interactive_debug())

Each query() call on the same client builds on the previous context. Claude remembers files it read, analysis it did, and commands it ran.

Subagents for Specialized Tasks

For complex workflows, you can define specialized subagents that your main agent delegates to. The main agent uses the Task tool to spawn subagents, and each subagent has its own system prompt, tools, and model.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition, ResultMessage

async def main():
    options = ClaudeAgentOptions(
        allowed_tools=["Read", "Glob", "Grep", "Task"],
        agents={
            "security-reviewer": AgentDefinition(
                description="Reviews code for security vulnerabilities and OWASP issues.",
                prompt="Analyze the given code for security vulnerabilities. Check for SQL injection, XSS, CSRF, insecure deserialization, and hardcoded secrets. Report severity levels.",
                tools=["Read", "Glob", "Grep"],
            ),
            "test-writer": AgentDefinition(
                description="Writes unit tests for Python code.",
                prompt="Write pytest unit tests for the given code. Cover edge cases, error paths, and main functionality. Use fixtures and parametrize where appropriate.",
                tools=["Read", "Write", "Glob"],
                model="sonnet",
            ),
        },
    )

    async for message in query(
        prompt="Review src/ for security issues using the security-reviewer, then use the test-writer to add tests for any files with vulnerabilities",
        options=options,
    ):
        if isinstance(message, ResultMessage):
            print(f"Completed in {message.duration_ms}ms, cost: ${message.total_cost_usd:.4f}")

asyncio.run(main())

The model field on AgentDefinition accepts "sonnet", "opus", "haiku", or "inherit". Use a cheaper model for simpler subagent tasks to keep costs down.

Extended Thinking

For complex reasoning tasks – debugging tricky code, multi-step planning, or architectural decisions – enable extended thinking so Claude can reason through the problem before responding.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ThinkingBlock, TextBlock

async def main():
    options = ClaudeAgentOptions(
        max_thinking_tokens=10000,
        allowed_tools=["Read", "Glob", "Grep"],
    )

    async for message in query(
        prompt="Analyze the architecture of this codebase and suggest how to refactor the database layer for better testability",
        options=options,
    ):
        if isinstance(message, AssistantMessage):
            for block in message.content:
                if isinstance(block, ThinkingBlock):
                    print(f"[Thinking] {block.thinking[:200]}...")
                if isinstance(block, TextBlock):
                    print(block.text)

asyncio.run(main())

Set max_thinking_tokens as an integer directly on ClaudeAgentOptions. The minimum budget is 1024 tokens. Start small and increase if you need deeper reasoning. Don’t set it for simple tasks – it adds latency and cost.

Hooks for Safety and Logging

Hooks let you intercept the agent loop at specific points. Use them to block dangerous commands, log tool usage for auditing, or inject context.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, HookMatcher

BLOCKED_COMMANDS = ["rm -rf", "DROP TABLE", "shutdown", "mkfs"]

async def validate_bash(input_data, tool_use_id, context):
    command = input_data.get("tool_input", {}).get("command", "")
    for pattern in BLOCKED_COMMANDS:
        if pattern in command:
            return {
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "deny",
                    "permissionDecisionReason": f"Blocked dangerous pattern: {pattern}",
                }
            }
    return {}

async def log_tool_use(input_data, tool_use_id, context):
    tool = input_data.get("tool_name", "unknown")
    print(f"[AUDIT] Tool called: {tool}")
    return {}

async def main():
    options = ClaudeAgentOptions(
        allowed_tools=["Read", "Write", "Bash"],
        permission_mode="acceptEdits",
        hooks={
            "PreToolUse": [
                HookMatcher(matcher="Bash", hooks=[validate_bash]),
                HookMatcher(hooks=[log_tool_use]),
            ],
        },
    )

    async for message in query(prompt="List files and create a summary", options=options):
        if hasattr(message, "result"):
            print(message.result)

asyncio.run(main())

The matcher field is a regex pattern. "Bash" matches only Bash tool calls. "Write|Edit" matches both. Omit matcher to match all tools.

How It Compares to the OpenAI Agents SDK

The OpenAI Agents SDK (released March 2025) focuses on multi-agent handoffs and orchestration – a triage agent routes to specialized agents. It’s lightweight and model-agnostic in theory, though it works best with OpenAI models.

The Claude Agent SDK takes a different approach: instead of a thin orchestration layer, it gives you a full agent runtime with real tools built in. You don’t define file reading or command execution – the SDK already has battle-tested implementations of those. The tradeoff is that the Claude Agent SDK is tightly coupled to Claude, while OpenAI’s SDK is more of a coordination framework.

Pick the Claude Agent SDK when you want an agent that can actually do things out of the box – read code, run tests, search the web, edit files. Pick OpenAI’s SDK when you’re building multi-agent routing systems and want to define every tool yourself.

Common Errors and Fixes

CLINotFoundError: Claude Code not found – The SDK bundles the Claude Code CLI, but if you installed with --no-deps or in a restricted environment, it might be missing. Reinstall cleanly:

1
pip install --force-reinstall claude-agent-sdk

ProcessError with exit code 1 – Usually means the API key is invalid or missing. Verify it:

1
2
3
echo $ANTHROPIC_API_KEY
# Should print your key. If empty:
export ANTHROPIC_API_KEY="sk-ant-..."

Agent loops forever without finishing – Set max_turns to cap iterations. A reasonable default for most tasks is 10-20 turns:

1
options = ClaudeAgentOptions(max_turns=15)

TypeError: unexpected keyword argument 'thinking' – The Agent SDK uses max_thinking_tokens as a flat integer, not the thinking dict from the Messages API. Use ClaudeAgentOptions(max_thinking_tokens=10000), not thinking={"type": "enabled", ...}.

Permission denied on tool calls – If Claude keeps asking for permission and blocking, set permission_mode. Use "acceptEdits" for file operations or "bypassPermissions" when running in CI:

1
2
3
4
options = ClaudeAgentOptions(
    allowed_tools=["Read", "Write", "Bash"],
    permission_mode="bypassPermissions",  # Only use in trusted environments
)

Cost spiraling on long sessions – Use max_budget_usd to set a hard spending cap:

1
options = ClaudeAgentOptions(max_budget_usd=1.00)

The ResultMessage at the end of each session reports total_cost_usd, so you can track spending per task.