Help us improve
Share bugs, ideas, or general feedback.
From claude-agent-sdk
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.
npx claudepluginhub ettrickshepherd/claude-agent-sdk-skillHow this skill is triggered — by the user, by Claude, or both
Slash command
/claude-agent-sdk:claude-agent-sdkThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
The Agent SDK provides Claude with autonomous tool execution. Unlike the Messages API where you implement the tool loop, agents execute tools automatically.
Implements Anthropic Claude Agent SDK for autonomous agents, subagents, tool orchestration, MCP servers, and multi-step workflows. Useful for session management, permissions, and errors like CLI not found or context exceeded.
Builds autonomous AI agents with Claude Agent SDK: computer use, tool calling, MCP integration, and production best practices for Anthropic models.
Guides Claude Agent SDK development in TypeScript/Python: auth, sessions, custom tools, permissions, prompts, tracking via docs-management delegation.
Share bugs, ideas, or general feedback.
The Agent SDK provides Claude with autonomous tool execution. Unlike the Messages API where you implement the tool loop, agents execute tools automatically.
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);
}
| 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 |
query() - One-off TasksCreates 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" # Auto-approve edits
)
):
if hasattr(message, 'result'):
print(message.result)
ClaudeSDKClient - ConversationsMaintains context across exchanges. Use for interactive applications.
async with ClaudeSDKClient(options=options) as client:
await client.connect() # Establish connection
await client.query("Read the auth module")
async for msg in client.receive_messages(): # Stream all messages
print(msg)
await client.query("Now refactor it") # Remembers context
async for msg in client.receive_messages():
print(msg)
await client.interrupt() # Cancel current operation
await client.rewind_files() # Restore file checkpoints
status = await client.get_mcp_status() # Check MCP server status
await client.disconnect() # Clean up
| 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 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):
# Fetch weather data
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 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.
Hooks can return permission decisions (checked in order):
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.
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" # "sonnet" | "opus" | "haiku" | "inherit"
),
"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.
Resume sessions to maintain context across queries:
session_id = None
# First query: capture session ID
async for message in query(prompt="Read auth.py", options=options):
if hasattr(message, 'subtype') and message.subtype == 'init':
session_id = message.session_id
# Resume with full context
async for message in query(
prompt="Find all callers", # "auth.py" understood from context
options=ClaudeAgentOptions(resume=session_id)
):
print(message)
ClaudeAgentOptions(
allowed_tools=["Read", "Edit"], # Whitelist tools
permission_mode="acceptEdits", # default|acceptEdits|plan|bypassPermissions
mcp_servers={"name": server}, # External MCP servers
cwd="/path/to/project", # Working directory
max_turns=10, # Prevent infinite loops
resume="session-id", # Resume session
hooks={...}, # Lifecycle hooks
agents={...}, # Subagent definitions
can_use_tool=permission_handler, # Programmatic permission callback (requires streaming input)
model="claude-sonnet-4-5-20250929", # Model selection (or "claude-sonnet-4-5", "claude-opus-4-5", "claude-haiku-4-5")
fallback_model="claude-haiku-4-5", # Fallback if primary model unavailable
system_prompt="Custom instructions", # Override system prompt
max_budget_usd=1.0, # Maximum spend limit
max_thinking_tokens=10000, # Extended thinking budget
enable_file_checkpointing=True, # Enable file state snapshots for rewind
output_format={"type": "json", "schema": {...}}, # Structured JSON output
plugins=[...], # Plugin extensions
sandbox=True, # Run in sandboxed environment
setting_sources=[...], # Custom setting sources
env={"API_KEY": "..."} # Environment variables
)
from claude_agent_sdk import (
CLINotFoundError, # Claude Code not installed
CLIConnectionError, # Connection issues
ProcessError, # Process failed
CLIJSONDecodeError # JSON parsing issues
)
try:
async for message in query(prompt="Task", options=options):
pass
except ProcessError as e:
print(f"Failed with exit code: {e.exit_code}")
from claude_agent_sdk import ResultMessage, AssistantMessage
# Stream yields different message types
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 # IMPORTANT: query() does not auto-terminate after ResultMessage
export ANTHROPIC_API_KEY=your-keyCLAUDE_CODE_USE_BEDROCK=1 + AWS credentialsCLAUDE_CODE_USE_VERTEX=1 + GCP credentialsCLAUDE_CODE_USE_FOUNDRY=1 + Azure credentialsRoute 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="" # Must be explicitly empty
The Agent SDK inherits Claude Code's model override environment variables, so you can route to different OpenRouter models:
# Route to specific models via OpenRouter
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.
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"]
}
}
)
Enable file state snapshots for safe rollback:
options = ClaudeAgentOptions(
enable_file_checkpointing=True,
allowed_tools=["Read", "Edit", "Write"]
)
# Later, rewind all file changes made by the agent
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
# If results are unsatisfactory:
await client.rewind_files() # Restores all files to pre-query state
Enable deeper reasoning with thinking token budgets:
options = ClaudeAgentOptions(
max_thinking_tokens=10000, # Budget for extended thinking
allowed_tools=["Read", "Glob", "Grep"]
)
The TypeScript SDK's Query object (returned by query()) exposes additional control methods:
const q = query({ prompt: "Analyze codebase", options });
// Control methods
await q.interrupt(); // Cancel current operation
await q.rewindFiles(); // Restore file checkpoints
await q.setPermissionMode("acceptEdits");
await q.setModel("claude-opus-4-5");
await q.setMaxThinkingTokens(10000);
// Informational methods
const commands = await q.supportedCommands();
const models = await q.supportedModels();
const mcpStatus = await q.mcpServerStatus();
const account = await q.accountInfo();
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 toolsThe 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)allow - Most reliable for headless agentsWithout 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 ResultMessageThe 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.
# CORRECT
async for message in query(prompt=generate_messages(), options=options):
if isinstance(message, ResultMessage):
result = message
break # Required — prevents deadlock
See custom-tools.md Known Issues for the full stream_done pattern.
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.
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 bugWhen 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)