The Claude Agent SDK is the same agent loop that powers Claude Code, exposed as a Python and TypeScript library. You install it, set ANTHROPIC_API_KEY, call query(), and Claude handles tool execution, context management, and stopping for you. This tutorial walks through building a real research agent that takes a topic, runs WebSearch, fetches pages, and saves a markdown report -- in under 100 lines of Python, for about $0.04 per run on Claude Sonnet 4.5. Updated May 2026.

What is the Claude Agent SDK and what does it give you out of the box?

The Claude Agent SDK is a Python and TypeScript library that exposes the agent loop, built-in tools, and context management that power Claude Code. You write the prompt and the policies, the SDK runs the loop. It was renamed from the Claude Code SDK in late 2025 (see the official overview).

Out of the box you get:

  • Built-in tools: Read, Write, Edit, Bash, Glob, Grep, WebSearch, WebFetch, Monitor, AskUserQuestion.
  • Agent loop: Claude calls tools, observes results, plans the next step, and stops on its own.
  • Hooks: PreToolUse, PostToolUse, Stop, SessionStart, SessionEnd -- callbacks for logging, auditing, or blocking.
  • Subagents: spawn specialised agents from a parent (AgentDefinition).
  • MCP: connect external tools via the Model Context Protocol, including in-process Python tools via create_sdk_mcp_server.
  • Sessions: resume or fork conversations by session_id.
  • Permissions: allowed_tools, disallowed_tools, and permission_mode for non-interactive runs.

The contrast with the Anthropic Client SDK is sharp: with the Client SDK you implement the while response.stop_reason == 'tool_use' loop. With the Agent SDK that loop is gone.

How do you install and authenticate the Claude Agent SDK?

Install the package, export your API key, and you are done. No separate Claude Code install -- the CLI binary is bundled.

Python (3.10+):

pip install claude-agent-sdk
export ANTHROPIC_API_KEY=sk-ant-...

TypeScript / Node 18+:

npm install @anthropic-ai/claude-agent-sdk
export ANTHROPIC_API_KEY=sk-ant-...

Get a key from the Claude Console. The SDK also authenticates against Amazon Bedrock (CLAUDE_CODE_USE_BEDROCK=1), Google Vertex AI (CLAUDE_CODE_USE_VERTEX=1), and Microsoft Azure Foundry (CLAUDE_CODE_USE_FOUNDRY=1).

Note: per Anthropic's terms, you cannot resell claude.ai logins to your end users. Use API key auth in production agents.

If Opus 4.7 throws a thinking.type.enabled error, upgrade to SDK v0.2.111+ (called out in the overview docs).

What is the minimum code needed to run an agent loop?

Six lines. The query() async iterator yields messages until the agent decides to stop:

import asyncio
from claude_agent_sdk import query

async def main():
    async for message in query(prompt="What files are in this directory?"):
        print(message)

asyncio.run(main())

That is the entire control flow. Claude reads the prompt, decides it needs Bash or Glob, runs the tool, observes the output, and emits assistant + tool-result messages until it produces a final answer.

For anything beyond a hello-world, pass ClaudeAgentOptions:

from claude_agent_sdk import query, ClaudeAgentOptions

options = ClaudeAgentOptions(
    system_prompt="You are a senior code reviewer.",
    allowed_tools=["Read", "Glob", "Grep"],
    permission_mode="acceptEdits",
    max_turns=15,
)

The message stream contains four shapes you actually use: SystemMessage (init events with session_id), AssistantMessage (text + tool calls), UserMessage (tool results), and ResultMessage (the final summary with total_cost_usd, num_turns, stop_reason).

How do you build a real research agent in under 100 lines?

Below is the full source for research_agent.py. It takes a topic, runs WebSearch and WebFetch, writes a markdown report to ./reports/, and prints cost + turn count. 96 lines including imports and blank lines.

# research_agent.py -- Claude Agent SDK, Python 3.10+
import asyncio
import sys
from pathlib import Path

from claude_agent_sdk import (
    query,
    tool,
    create_sdk_mcp_server,
    ClaudeAgentOptions,
    AssistantMessage,
    TextBlock,
    ResultMessage,
    SystemMessage,
)

REPORTS_DIR = Path("./reports")
REPORTS_DIR.mkdir(exist_ok=True)


@tool(
    "save_report",
    "Save the final research report to disk as a markdown file.",
    {"topic": str, "markdown": str},
)
async def save_report(args):
    slug = args["topic"].lower().replace(" ", "-")[:60]
    path = REPORTS_DIR / f"{slug}.md"
    path.write_text(args["markdown"])
    return {"content": [{"type": "text", "text": f"Saved {path}"}]}


research_server = create_sdk_mcp_server(
    name="research", version="1.0.0", tools=[save_report]
)

SYSTEM = """You are a research agent. For the given topic:
1. Run WebSearch to find 5-7 high-quality, recent sources.
2. Use WebFetch on the 3-4 most relevant URLs.
3. Synthesise a 400-600 word markdown report with inline links.
4. Call save_report once. Do NOT call it twice. Then stop.
"""


async def research(topic: str) -> None:
    options = ClaudeAgentOptions(
        system_prompt=SYSTEM,
        allowed_tools=[
            "WebSearch",
            "WebFetch",
            "mcp__research__save_report",
        ],
        mcp_servers={"research": research_server},
        permission_mode="acceptEdits",
        max_turns=20,
    )

    async for message in query(prompt=f"Research: {topic}", options=options):
        if isinstance(message, SystemMessage) and message.subtype == "init":
            print(f"[session] {message.data['session_id']}")
        elif isinstance(message, AssistantMessage):
            for block in message.content:
                if isinstance(block, TextBlock):
                    print(block.text[:200])
        elif isinstance(message, ResultMessage):
            print(
                f"[done] cost=${message.total_cost_usd:.4f} "
                f"turns={message.num_turns} stop={message.stop_reason}"
            )


if __name__ == "__main__":
    topic = " ".join(sys.argv[1:]) or "how AI search engines pick citation sources"
    asyncio.run(research(topic))

Run it: python research_agent.py "how AI search engines pick citation sources". The agent does the rest.

How do you give your agent custom tools?

Use the @tool decorator and register the function with create_sdk_mcp_server. The SDK runs the tool in-process -- no subprocess, no IPC, no separate MCP server to deploy.

The four parts of a custom tool, per the custom-tools docs: name, description, input schema, handler. In Python:

from claude_agent_sdk import tool, create_sdk_mcp_server

@tool(
    "save_report",                            # 1. name
    "Save the report to disk as markdown.",   # 2. description
    {"topic": str, "markdown": str},          # 3. input schema
)
async def save_report(args):                  # 4. handler
    Path(f"./reports/{args['topic']}.md").write_text(args["markdown"])
    return {"content": [{"type": "text", "text": "Saved."}]}

server = create_sdk_mcp_server(
    name="research", version="1.0.0", tools=[save_report]
)

Then wire the server into options. The footgun: you must reference the tool by its MCP-prefixed name in allowed_tools, not by the function name:

options = ClaudeAgentOptions(
    mcp_servers={"research": server},
    allowed_tools=["mcp__research__save_report"],  # mcp__<server>__<tool>
)

Miss the mcp__research__ prefix and Claude never sees the tool. We hit this on attempt one.

How do you stop the agent and inspect its trace?

Two stop levers and one inspection point. Use max_turns for a hard cap, the Stop hook for conditional stops, and the ResultMessage for the trace summary.

1. Hard cap with max_turns. Without this, an agent can spin for tens of dollars on a single task:

ClaudeAgentOptions(max_turns=20)

2. Conditional stops with the Stop hook, useful for budget guards:

from claude_agent_sdk import HookMatcher

async def budget_guard(input_data, tool_use_id, context):
    if context.total_cost_usd > 0.50:
        return {"decision": "block", "reason": "Budget exceeded"}
    return {}

options = ClaudeAgentOptions(
    hooks={"Stop": [HookMatcher(matcher="*", hooks=[budget_guard])]}
)

3. Inspect the trace via ResultMessage. Per the cost-tracking docs, ResultMessage carries subtype, duration_ms, duration_api_ms, is_error, num_turns, session_id, total_cost_usd, usage, result, stop_reason, and model_usage. Log them:

[session] 8f3c2a1e-...
[done] cost=$0.0412 turns=11 stop=end_turn

For full step-by-step traces, capture every AssistantMessage (including ToolUseBlocks) and every UserMessage tool-result. That gives you the (thought -> tool_call -> observation) triplet for each turn, which is what you actually want when an agent goes off the rails.

What does a Claude Agent SDK run actually cost?

$0.04 per run for our 11-turn research agent on Claude Sonnet 4.5. Per the Anthropic pricing page, Sonnet 4.5 is $3.00 per million input tokens, $15.00 per million output tokens, and SDK-triggered web search is $10 per 1,000 searches.

Measured breakdown for one run on the topic how AI search engines pick citation sources:

Item Quantity Cost
Input tokens (prompt + tool results) ~4,000 $0.012
Output tokens (reasoning + report) ~1,600 $0.024
WebSearch calls 5 $0.005
Total -- $0.041

Two cost levers worth knowing:

  • Prompt caching cuts repeated input cost by up to 90% (cache reads bill at 0.1x base input rate). If your system prompt is stable, caching pays for itself by run two.
  • Batch API discounts both input and output tokens by 50% for asynchronous workloads.

If you skip max_turns, a runaway loop on Sonnet 4.5 can hit $1-3 before you notice. Set the cap.

Cost per Research Agent Run (Claude Sonnet 4.5)
Input tokens ($3/MTok)
0.012$
Output tokens ($15/MTok)
0.024$
Web searches ($10/1K)
0.005$
Total per run
0.041$
Source: Anthropic Pricing + measured run, May 2026

What broke when we first ran this agent?

Three things, all real, all from the first afternoon of building this. None are in the marketing pages. All are in the docs if you read carefully.

1. The MCP tool name prefix. We registered save_report and listed "save_report" in allowed_tools. Claude wrote a beautiful report, then printed "I would save this if I had a save tool." The tool name in allowed_tools must be mcp__<server>__<tool> -- in our case mcp__research__save_report. Fix: one string change.

2. The agent hung waiting for permission. Default permission_mode prompts for approval on Write/Edit/Bash. In a non-interactive script that means the process blocks on stdin forever. Fix: set permission_mode="acceptEdits" (or "bypassPermissions" if you really mean it). For production, gate with a PreToolUse hook instead.

3. The agent saved the report twice, then a third time. With no instruction to stop, it kept refining. Two fixes stacked: (a) the system prompt now says "Call save_report once. Do NOT call it twice. Then stop." and (b) max_turns=20 caps the loop hard. Belt and suspenders -- LLMs interpret "once" generously when they have tokens left.

GE rule of thumb: every agent gets max_turns, an explicit stop instruction in the system prompt, and a budget hook before it sees production traffic.

When should you reach for the Agent SDK vs the Client SDK or Managed Agents?

Pick by where the agent runs and who owns the loop.

Tool You write the tool loop? Runs in Best for
Anthropic Client SDK Yes Your process Fine-grained control, custom orchestration, non-Claude-Code patterns
Claude Agent SDK No Your process Local prototypes, CI agents, anything that touches your filesystem
Managed Agents No Anthropic sandbox Long-running production agents, no infra to operate

The Agent SDK overview recommends prototyping locally with the Agent SDK, then graduating to Managed Agents when you need hosted sessions, sandboxes, or async long-running runs.

If you are still hand-rolling a while stop_reason == 'tool_use' loop on the Client SDK and not benefiting from custom orchestration, you are paying engineering tax for something the Agent SDK gives you free.

FeatureAnthropic Client SDKClaude Agent SDKManaged Agents
Tool execution loopYou write itBuilt inBuilt in
Built-in toolsNoneRead, Write, Bash, WebSearch, WebFetch, Glob, Grep, EditSame as Agent SDK
Runs inYour processYour processAnthropic sandbox
Custom toolsFunction calling, you execute@tool decorator, in-process MCPTriggered by Claude, executed by you
Session persistenceYou implementJSONL on diskAnthropic-hosted event log
Best forCustom orchestrationLocal prototypes, CI, filesystem agentsLong-running production agents
PricingToken ratesToken rates (no SDK fee)Token rates + sandbox