Claude Agent SDK
Official Python SDK for building AI agents with Claude Code
Bidirectional communication, custom tools via MCP, hooks for control flow, permission callbacks, and session management.
Installation
pip install claude-agent-sdk
# Claude Code CLI is bundled - no separate install neededTwo Entry Points
Choose based on your use case
query()
One-shot, fire-and-forget. Send prompt, receive all responses. No state management.
from claude_agent_sdk import query
async for msg in query(prompt="What is 2+2?"):
print(msg)ClaudeSDKClient
Interactive, bidirectional. Maintains state, supports hooks, can interrupt.
from claude_agent_sdk import ClaudeSDKClient
async with ClaudeSDKClient() as client:
await client.query("First question")
async for msg in client.receive_response():
print(msg)
await client.query("Follow-up")
async for msg in client.receive_response():
print(msg)ClaudeAgentOptions
All configuration options
from claude_agent_sdk import ClaudeAgentOptions
options = ClaudeAgentOptions(
# Tools
tools=["Bash", "Read", "Write"], # Available tools
allowed_tools=["mcp__calc__add"], # Explicitly allowed
disallowed_tools=["Bash"], # Explicitly blocked
# Prompts & Behavior
system_prompt="You are a helpful assistant",
max_turns=10, # Max conversation turns
max_budget_usd=5.0, # Cost limit
max_thinking_tokens=8000, # Reasoning token limit
# Models
model="claude-sonnet-4-5",
fallback_model="claude-haiku-3-5",
# Permissions
permission_mode="default", # or "acceptEdits", "bypassPermissions"
can_use_tool=my_permission_callback, # Custom permission function
# Sessions
continue_conversation=True,
resume="session-id-123", # Resume previous session
fork_session=True, # Branch from session
# MCP Servers
mcp_servers={"calc": calculator_server},
# Hooks
hooks={
"PreToolUse": [HookMatcher(...)],
"PostToolUse": [HookMatcher(...)]
},
# Custom Agents
agents={"reviewer": AgentDefinition(...)},
# Working directory
cwd="/path/to/project",
)permission_modeControls tool approval flow
system_promptSystem prompt or preset
output_formatStructured output schema
Message Types
What you receive from queries
AssistantMessage
Claude's response with content blocks
content: list[ContentBlock]model: strparent_tool_use_id: str | NoneUserMessage
User input (for context)
content: str | list[ContentBlock]parent_tool_use_id: str | NoneSystemMessage
System events and notifications
subtype: strdata: dictResultMessage
Final result with metrics
duration_ms: inttotal_cost_usd: floatnum_turns: intsession_id: strstructured_output: AnyContent Block Types
TextBlocktext: strThinkingBlockthinking: strsignature: strToolUseBlockid: strname: strinput: dictToolResultBlocktool_use_id: strcontent: stris_error: boolCustom Tools (MCP)
In-process tool servers
SDK MCP Servers
Run tools in-process (no subprocess). Better performance, easier debugging, direct access to app state.
Defining Tools
from claude_agent_sdk import tool, create_sdk_mcp_server
@tool("add", "Add two numbers", {"a": float, "b": float})
async def add(args):
result = args["a"] + args["b"]
return {
"content": [{"type": "text", "text": f"Result: {result}"}]
}
@tool("multiply", "Multiply two numbers", {"a": float, "b": float})
async def multiply(args):
result = args["a"] * args["b"]
return {
"content": [{"type": "text", "text": f"Result: {result}"}],
"is_error": False # Optional
}
# Create server
calculator = create_sdk_mcp_server(
name="calculator",
version="1.0.0",
tools=[add, multiply]
)Using Tools
options = ClaudeAgentOptions(
mcp_servers={"calc": calculator},
allowed_tools=["mcp__calc__add", "mcp__calc__multiply"]
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Calculate 15 * 8 + 12")
async for msg in client.receive_response():
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(block.text)
# Tool naming: mcp__<server>__<tool>
# e.g., mcp__calc__addTool Input Schema Options
# Simple dict
{"name": str, "age": int, "score": float}
# Full JSON Schema
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["name"]
}
# TypedDict
from typing_extensions import TypedDict
class MyInput(TypedDict):
name: str
age: intTool Return Format
# Text result
return {
"content": [{"type": "text", "text": "Result here"}]
}
# Image result
return {
"content": [{
"type": "image",
"data": "base64_encoded_data",
"mimeType": "image/png"
}]
}
# Error result
return {
"content": [{"type": "text", "text": "Error: something failed"}],
"is_error": True
}Permission Callbacks
Fine-grained tool access control
Permission Callback Pattern
from claude_agent_sdk import (
PermissionResultAllow,
PermissionResultDeny,
ToolPermissionContext
)
async def my_permission_callback(
tool_name: str,
input_data: dict,
context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
"""Control which tools Claude can use."""
# Allow read-only operations
if tool_name in ["Read", "Glob", "Grep"]:
return PermissionResultAllow()
# Block writes to system directories
if tool_name in ["Write", "Edit"]:
path = input_data.get("file_path", "")
if path.startswith("/etc/") or path.startswith("/usr/"):
return PermissionResultDeny(
message=f"Cannot write to system directory: {path}"
)
return PermissionResultAllow()
# Modify inputs (redirect to safe location)
if tool_name == "Write":
safe_path = f"./output/{input_data['file_path'].split('/')[-1]}"
return PermissionResultAllow(
updated_input={**input_data, "file_path": safe_path}
)
# Check bash commands
if tool_name == "Bash":
command = input_data.get("command", "")
dangerous = ["rm -rf", "sudo", "chmod 777"]
for pattern in dangerous:
if pattern in command:
return PermissionResultDeny(
message=f"Dangerous pattern: {pattern}"
)
return PermissionResultAllow()
# Default: deny
return PermissionResultDeny(message="Tool not allowed")
# Use callback
options = ClaudeAgentOptions(
can_use_tool=my_permission_callback,
permission_mode="default" # Required for callbacks
)PermissionResultAllow
PermissionResultAllow(
# Modify tool inputs
updated_input={"file_path": "/safe/path"},
# Update global permissions
updated_permissions=[...]
)PermissionResultDeny
PermissionResultDeny(
message="Reason for denial",
# Stop entire session (not just this tool)
interrupt=True
)Hooks System
Control flow at specific points
Hook Events
PreToolUseBefore tool execution
PostToolUseAfter tool execution
UserPromptSubmitUser submits prompt
StopStop event
SubagentStopSubagent stop
PreCompactBefore transcript compaction
Hook Implementation
from claude_agent_sdk import HookMatcher, HookInput, HookContext, HookJSONOutput
async def check_bash_command(
input_data: HookInput,
tool_use_id: str | None,
context: HookContext
) -> HookJSONOutput:
"""Block dangerous bash commands."""
if input_data["tool_name"] != "Bash":
return {}
command = input_data["tool_input"].get("command", "")
if "rm -rf" in command:
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Dangerous command blocked"
}
}
return {}
async def review_output(
input_data: HookInput,
tool_use_id: str | None,
context: HookContext
) -> HookJSONOutput:
"""Review tool output."""
response = input_data.get("tool_response", "")
if "error" in str(response).lower():
return {
"systemMessage": "Tool produced an error",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Consider a different approach"
}
}
return {}
# Configure hooks
options = ClaudeAgentOptions(
hooks={
"PreToolUse": [
HookMatcher(
matcher="Bash", # Match specific tool
hooks=[check_bash_command],
timeout=30.0
)
],
"PostToolUse": [
HookMatcher(
matcher=None, # Match all tools
hooks=[review_output]
)
]
}
)Hook Output Options
# Control execution flow
{
"continue_": False, # Stop after hook (default: True)
"stopReason": "Error detected",
"suppressOutput": True # Hide from transcript
}
# Block/approve (PreToolUse)
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow" | "deny" | "ask",
"permissionDecisionReason": "Reason"
}
}
# Modify inputs (PreToolUse)
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"updatedInput": {"file_path": "/safe/path"}
}
}
# Add context (PostToolUse)
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Extra info for Claude"
}
}
# Defer (async)
{
"async_": True,
"asyncTimeout": 5000 # milliseconds
}Custom Agents
Specialized agents with custom prompts
from claude_agent_sdk import AgentDefinition
options = ClaudeAgentOptions(
agents={
"code-reviewer": AgentDefinition(
description="Reviews code for best practices",
prompt="""You are a code reviewer. Analyze code for:
- Bugs and security vulnerabilities
- Performance issues
- Best practice violations
Provide constructive feedback.""",
tools=["Read", "Grep"],
model="sonnet"
),
"doc-writer": AgentDefinition(
description="Writes comprehensive documentation",
prompt="Write clear documentation with examples.",
tools=["Read", "Write", "Edit"],
model="sonnet"
),
"tester": AgentDefinition(
description="Writes and runs tests",
prompt="Write comprehensive tests with good coverage.",
tools=["Read", "Write", "Bash"],
model="haiku"
)
}
)
# Use agent in prompt
async for msg in query(
prompt="Use the code-reviewer agent to review src/main.py",
options=options
):
print(msg)Session Management
Resume and fork conversations
Resume Session
# Get session_id from ResultMessage
previous_session = "session-xyz-123"
options = ClaudeAgentOptions(
resume=previous_session,
continue_conversation=True
)
async for msg in query(
prompt="Continue where we left off",
options=options
):
print(msg)Fork Session
# Branch from a point
options = ClaudeAgentOptions(
resume=original_session,
fork_session=True # New session ID
)
async for msg in query(
prompt="Try a different approach",
options=options
):
print(msg)Structured Output
Validated JSON responses
options = ClaudeAgentOptions(
output_format={
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string", "format": "email"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["name", "email"]
}
}
)
async for msg in query(
prompt="Extract: John Doe, john@example.com, 30",
options=options
):
if isinstance(msg, ResultMessage):
print(msg.structured_output)
# {"name": "John Doe", "email": "john@example.com", "age": 30}ClaudeSDKClient Methods
Interactive session control
connect(prompt?)Connect to Claude with optional initial prompt
query(prompt)Send a message
receive_messages()Receive all messages (async iterator)
receive_response()Receive until ResultMessage
interrupt()Send interrupt signal
set_permission_mode(mode)Change permission mode mid-conversation
set_model(model)Change AI model mid-conversation
get_server_info()Get server initialization info
disconnect()Disconnect from Claude
Dynamic Settings
async with ClaudeSDKClient(options) as client:
await client.query("Start with sonnet")
async for msg in client.receive_response():
pass
# Switch to faster model
await client.set_model("claude-haiku-3-5")
# Change permissions
await client.set_permission_mode("acceptEdits")
await client.query("Continue with haiku")
async for msg in client.receive_response():
passError Handling
from claude_agent_sdk import (
ClaudeSDKError, # Base error
CLINotFoundError, # CLI not installed
CLIConnectionError, # Connection issues
ProcessError, # Process failed
CLIJSONDecodeError, # JSON parse error
)
try:
async for msg in query(prompt="Hello"):
pass
except CLINotFoundError:
print("Install: pip install claude-agent-sdk")
except CLIConnectionError as e:
print(f"Connection error: {e}")
except ProcessError as e:
print(f"Process failed (exit {e.exit_code}): {e.stderr}")
except CLIJSONDecodeError as e:
print(f"JSON parse error: {e.line}")Complete Examples
Simple Query
import anyio
from claude_agent_sdk import query, AssistantMessage, TextBlock
async def main():
async for msg in query(prompt="What is Python?"):
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(block.text)
anyio.run(main)Interactive Chat
import asyncio
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
AssistantMessage,
ResultMessage,
TextBlock
)
async def chat():
options = ClaudeAgentOptions(
system_prompt="You are a helpful Python expert"
)
async with ClaudeSDKClient(options=options) as client:
while True:
user_input = input("You: ")
if user_input.lower() == "quit":
break
await client.query(user_input)
print("Claude: ", end="")
async for msg in client.receive_response():
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(block.text)
elif isinstance(msg, ResultMessage):
print(f"\n[Cost: ${msg.total_cost_usd:.4f}]\n")
asyncio.run(chat())Tool + Permission + Hook
import asyncio
from claude_agent_sdk import (
ClaudeSDKClient, ClaudeAgentOptions,
tool, create_sdk_mcp_server,
HookMatcher,
PermissionResultAllow, PermissionResultDeny,
AssistantMessage, TextBlock
)
# Tool
@tool("search", "Search database", {"query": str})
async def search(args):
results = f"Found: {args['query']}"
return {"content": [{"type": "text", "text": results}]}
server = create_sdk_mcp_server("db", tools=[search])
# Permission callback
async def check_permission(tool_name, input_data, ctx):
if tool_name == "mcp__db__search":
query = input_data.get("query", "")
if "DROP" in query.upper():
return PermissionResultDeny(message="SQL injection blocked")
return PermissionResultAllow()
# Hook
async def log_search(input_data, tool_use_id, ctx):
print(f"[LOG] Searching: {input_data['tool_input']}")
return {}
options = ClaudeAgentOptions(
mcp_servers={"db": server},
allowed_tools=["mcp__db__search"],
can_use_tool=check_permission,
hooks={
"PreToolUse": [
HookMatcher(matcher="mcp__db__search", hooks=[log_search])
]
}
)
async def main():
async with ClaudeSDKClient(options=options) as client:
await client.query("Search for Python tutorials")
async for msg in client.receive_response():
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(block.text)
asyncio.run(main())Key Takeaways
Two entry points
query() for simple one-shot queries. ClaudeSDKClient for interactive conversations with hooks and permissions.
In-process tools
SDK MCP servers run in your process. No subprocess management, better performance, direct state access.
Fine-grained control
Permission callbacks for tool access. Hooks for pre/post execution. Can modify inputs, block tools, add context.
Session persistence
Resume conversations with session IDs. Fork to explore alternatives. Full conversation history preserved.