| name | claude-agent-sdk |
| description | Build production AI agents with the Claude Agent SDK. Use when working with: (1) Creating autonomous agents with query() or ClaudeSDKClient, (2) Defining custom tools via in-process MCP servers, (3) Implementing hooks for tool interception and permissions, (4) Building multi-agent systems with subagents, (5) Managing sessions and state across queries, (6) Integrating MCP servers for external capabilities. Covers Python and TypeScript SDKs with production patterns.
|
Claude Agent SDK
The Agent SDK provides Claude with autonomous tool execution. Unlike the Messages API where you implement the tool loop, agents execute tools automatically.
Quick Start
Python:
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def main():
async for message in query(
prompt="Find and fix the bug in auth.py",
options=ClaudeAgentOptions(allowed_tools=["Read", "Edit", "Bash"])
):
print(message)
asyncio.run(main())
TypeScript:
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Find and fix the bug in auth.py",
options: { allowedTools: ["Read", "Edit", "Bash"] }
})) {
console.log(message);
}
When to Use Agent SDK vs Messages API
| Scenario | Use |
|---|
| Autonomous multi-step tasks | Agent SDK |
| File operations, code editing | Agent SDK |
| Simple Q&A, text generation | Messages API |
| Custom tool execution control | Messages API |
| Browser automation, web tasks | Agent SDK |
Core APIs
query() - One-off Tasks
Creates new session each time. Best for independent tasks.
async for message in query(
prompt="Your task here",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Write", "Edit", "Bash"],
permission_mode="acceptEdits"
)
):
if hasattr(message, 'result'):
print(message.result)
ClaudeSDKClient - Conversations
Maintains context across exchanges. Use for interactive applications.
async with ClaudeSDKClient(options=options) as client:
await client.connect()
await client.query("Read the auth module")
async for msg in client.receive_messages():
print(msg)
await client.query("Now refactor it")
async for msg in client.receive_messages():
print(msg)
await client.interrupt()
await client.rewind_files()
status = await client.get_mcp_status()
await client.disconnect()
Built-in Tools
| Tool | Purpose |
|---|
Read | Read files in working directory |
Write | Create new files |
Edit | Make precise edits to existing files |
Bash | Run terminal commands and scripts |
BashOutput | Retrieve output from background bash shells |
KillBash | Kill running background bash shells |
Glob | Find files by pattern (**/*.ts) |
Grep | Search file contents with regex |
NotebookEdit | Edit Jupyter notebook cells |
WebSearch | Search the web |
WebFetch | Fetch and parse web pages |
AskUserQuestion | Request user clarification |
TodoWrite | Create/manage structured task lists |
Task | Spawn subagents |
ExitPlanMode | Exit planning mode |
ListMcpResources | List available MCP resources |
ReadMcpResource | Read content from MCP resources |
Custom Tools
Custom tools use in-process MCP servers. Tool names follow: mcp__{server}__{tool}
Python:
from claude_agent_sdk import tool, create_sdk_mcp_server
@tool("get_weather", "Get temperature for coordinates",
{"latitude": float, "longitude": float})
async def get_weather(args):
return {"content": [{"type": "text", "text": f"72°F"}]}
server = create_sdk_mcp_server(
name="weather",
version="1.0.0",
tools=[get_weather]
)
options = ClaudeAgentOptions(
mcp_servers={"weather": server},
allowed_tools=["mcp__weather__get_weather"]
)
TypeScript:
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
const server = createSdkMcpServer({
name: "weather",
version: "1.0.0",
tools: [
tool("get_weather", "Get temperature", {
latitude: z.number(),
longitude: z.number()
}, async (args) => ({
content: [{ type: "text", text: "72°F" }]
}))
]
});
Important: Custom MCP tools require streaming input mode (async generator).
See custom-tools.md for complete patterns.
Hooks System
Hooks intercept agent execution at lifecycle points.
| Hook | When | Python | TypeScript |
|---|
PreToolUse | Before tool executes | Yes | Yes |
PostToolUse | After tool succeeds | Yes | Yes |
PostToolUseFailure | After tool fails | Partial* | Yes |
UserPromptSubmit | User submits prompt | Yes | Yes |
Stop | Agent stops | Yes | Yes |
SubagentStop | Subagent completes | Yes | Yes |
PreCompact | Before context compaction | Yes | Yes |
Notification | Status messages | No | Yes |
SessionStart | Session begins | No | Yes |
SessionEnd | Session ends | No | Yes |
SubagentStart | Subagent begins | No | Yes |
PermissionRequest | Permission prompt shown | No | Yes |
*PostToolUseFailure: Present in Python SDK types.py (HookEvent Literal) but not yet in the official HookInput union type or documentation. May work in recent SDK versions (>= ~0.1.26). Test with your specific version before relying on it.
Permission Decisions
Hooks can return permission decisions (checked in order):
- deny - Block immediately
- ask - Require user approval
- allow - Auto-approve
Example: Block .env modifications
async def protect_env(input_data, tool_use_id, context):
file_path = input_data['tool_input'].get('file_path', '')
if file_path.endswith('.env'):
return {
'hookSpecificOutput': {
'hookEventName': 'PreToolUse',
'permissionDecision': 'deny',
'permissionDecisionReason': 'Cannot modify .env files'
}
}
return {}
options = ClaudeAgentOptions(
hooks={'PreToolUse': [HookMatcher(matcher='Write|Edit', hooks=[protect_env])]}
)
See hooks-reference.md for all hooks and patterns.
Subagents
Spawn specialized agents for focused subtasks with parallelization.
options = ClaudeAgentOptions(
allowed_tools=["Read", "Glob", "Grep", "Task"],
agents={
"code-reviewer": AgentDefinition(
description="Expert code reviewer for quality and security",
prompt="Analyze code quality and suggest improvements",
tools=["Read", "Glob", "Grep"],
model="sonnet"
),
"test-writer": AgentDefinition(
description="Test generation specialist",
prompt="Write comprehensive unit tests",
tools=["Read", "Write", "Bash"],
model="sonnet"
)
}
)
See multi-agent.md for orchestration patterns.
Session Management
Resume sessions to maintain context across queries:
session_id = None
async for message in query(prompt="Read auth.py", options=options):
if hasattr(message, 'subtype') and message.subtype == 'init':
session_id = message.session_id
async for message in query(
prompt="Find all callers",
options=ClaudeAgentOptions(resume=session_id)
):
print(message)
Configuration Options
ClaudeAgentOptions(
allowed_tools=["Read", "Edit"],
permission_mode="acceptEdits",
mcp_servers={"name": server},
cwd="/path/to/project",
max_turns=10,
resume="session-id",
hooks={...},
agents={...},
can_use_tool=permission_handler,
model="claude-sonnet-4-5-20250929",
fallback_model="claude-haiku-4-5",
system_prompt="Custom instructions",
max_budget_usd=1.0,
max_thinking_tokens=10000,
enable_file_checkpointing=True,
output_format={"type": "json", "schema": {...}},
plugins=[...],
sandbox=True,
setting_sources=[...],
env={"API_KEY": "..."}
)
Security Best Practices
- Deny-by-default: Only whitelist necessary tools
- Confirm sensitive actions: Use hooks for git push, infrastructure changes
- Block dangerous commands: rm -rf, sudo, etc.
- Protect secrets: Never expose credentials in agent-visible context
- Use short-lived tokens: Rotate credentials aggressively
- Log all tool invocations: Audit trail with timestamps
Error Handling
from claude_agent_sdk import (
CLINotFoundError,
CLIConnectionError,
ProcessError,
CLIJSONDecodeError
)
try:
async for message in query(prompt="Task", options=options):
pass
except ProcessError as e:
print(f"Failed with exit code: {e.exit_code}")
Message Types
from claude_agent_sdk import ResultMessage, AssistantMessage
async for message in query(prompt="Task", options=options):
if isinstance(message, AssistantMessage):
print(message.content)
elif isinstance(message, ResultMessage):
if message.subtype == "success":
print(f"Completed: {message.result}")
print(f"Cost: ${message.total_cost_usd}")
break
Authentication
- Anthropic API:
export ANTHROPIC_API_KEY=your-key
- Amazon Bedrock:
CLAUDE_CODE_USE_BEDROCK=1 + AWS credentials
- Google Vertex AI:
CLAUDE_CODE_USE_VERTEX=1 + GCP credentials
- Microsoft Azure AI Foundry:
CLAUDE_CODE_USE_FOUNDRY=1 + Azure credentials
OpenRouter
Route Agent SDK requests through OpenRouter for access to multiple model providers, fallbacks, and cost optimization:
export ANTHROPIC_BASE_URL="https://openrouter.ai/api"
export ANTHROPIC_AUTH_TOKEN="$OPENROUTER_API_KEY"
export ANTHROPIC_API_KEY=""
The Agent SDK inherits Claude Code's model override environment variables, so you can route to different OpenRouter models:
export ANTHROPIC_DEFAULT_SONNET_MODEL="anthropic/claude-sonnet-4"
export ANTHROPIC_DEFAULT_OPUS_MODEL="anthropic/claude-opus-4"
Important: ANTHROPIC_API_KEY must be set to an empty string — if omitted entirely, the SDK will fail with an authentication error. The actual auth is handled by ANTHROPIC_AUTH_TOKEN.
Structured Outputs
Use output_format for validated JSON responses:
options = ClaudeAgentOptions(
output_format={
"type": "json",
"schema": {
"type": "object",
"properties": {
"summary": {"type": "string"},
"issues": {"type": "array", "items": {"type": "string"}},
"severity": {"type": "string", "enum": ["low", "medium", "high"]}
},
"required": ["summary", "issues", "severity"]
}
}
)
File Checkpointing
Enable file state snapshots for safe rollback:
options = ClaudeAgentOptions(
enable_file_checkpointing=True,
allowed_tools=["Read", "Edit", "Write"]
)
async with ClaudeSDKClient(options=options) as client:
await client.connect()
await client.query("Refactor the auth module")
async for msg in client.receive_messages():
pass
await client.rewind_files()
Extended Thinking
Enable deeper reasoning with thinking token budgets:
options = ClaudeAgentOptions(
max_thinking_tokens=10000,
allowed_tools=["Read", "Glob", "Grep"]
)
TypeScript Query Object Methods
The TypeScript SDK's Query object (returned by query()) exposes additional control methods:
const q = query({ prompt: "Analyze codebase", options });
await q.interrupt();
await q.rewindFiles();
await q.setPermissionMode("acceptEdits");
await q.setModel("claude-opus-4-5");
await q.setMaxThinkingTokens(10000);
const commands = await q.supportedCommands();
const models = await q.supportedModels();
const mcpStatus = await q.mcpServerStatus();
const account = await q.accountInfo();
Known Issues & Troubleshooting
In-process MCP servers unreliable with subagents
create_sdk_mcp_server() can fail with "Stream closed" when used with parallel subagents due to message-queue backpressure. The SDK's internal transport gets blocked when multiple subagents compete for the MCP connection.
Recommendation: Use stdio subprocess MCP servers for production multi-agent setups. In-process servers are fine for single-agent simple use cases. (Python SDK #425, TS SDK #41)
acceptEdits does NOT auto-approve MCP tools
The acceptEdits permission mode only auto-approves file operations (Edit, Write) and filesystem Bash commands (mkdir, rm, mv, cp). MCP tools still require explicit permission resolution.
Options for MCP tool approval:
bypassPermissions - Approves everything (use with caution)
can_use_tool callback - Programmatic per-tool decisions (requires streaming input mode)
- PreToolUse hooks returning
allow - Most reliable for headless agents
"Stream closed" in headless mode
Without a can_use_tool callback or explicit PreToolUse allow decisions, tools requiring permission trigger an interactive prompt. When running the SDK without a terminal (headless/server mode), this fails with "Stream closed" because there is no UI to display the prompt.
Workaround: Use PreToolUse hooks to return explicit allow/deny decisions for all tools that would otherwise trigger a permission prompt. See hooks-reference.md for permission interaction details.
query() does NOT auto-terminate after ResultMessage
The async for loop over query() does not end when ResultMessage arrives. Internally, query() waits for an "end" event from _read_messages(), which only fires after the transport closes. You must break explicitly after ResultMessage.
With a plain string prompt this may appear to work (the stream eventually closes), but with streaming input (async generator + stream_done pattern), omitting break causes a circular deadlock: the loop waits for the stream to close → the stream waits for the generator to finish → the generator waits for stream_done.set() in finally → finally only runs after the loop exits.
async for message in query(prompt=generate_messages(), options=options):
if isinstance(message, ResultMessage):
result = message
break
See custom-tools.md Known Issues for the full stream_done pattern.
Generator exhaustion causes "Stream closed"
When using query() with an async generator prompt, the SDK closes stdin after the generator finishes yielding + a 60-second timeout. For long-running agents, this causes "Stream closed" errors on hooks and can_use_tool callbacks even when permissions are correctly configured.
Fix: Keep the generator alive with an asyncio.Event until the agent completes. See custom-tools.md Known Issues for the full stream_done pattern.
Background subagents can't access MCP tools
Subagents spawned with run_in_background: true silently fail when calling MCP tools. The background process doesn't inherit MCP server connections from the parent agent. Use foreground subagents for MCP-dependent tasks. (Claude Code #13254)
can_use_tool empty arguments bug
When using the can_use_tool callback, always pass updated_input=tool_input (the original arguments) in PermissionResultAllow. Omitting updated_input causes the tool to receive empty arguments. (Python SDK #320)
Reference Files