claude-agent-sdk
Claude Agent SDK
Provides patterns and best practices for building production AI agents using the Claude Agent SDK (Python: claude-agent-sdk, TypeScript: @anthropic-ai/claude-agent-sdk).
Installation
# TypeScript
npm install @anthropic-ai/claude-agent-sdk
# Python (uv — recommended)
uv add claude-agent-sdk
# Python (pip)
pip install claude-agent-sdk
Set the API key before running any agent:
export ANTHROPIC_API_KEY=your-api-key
Third-party providers are also supported: set CLAUDE_CODE_USE_BEDROCK=1, CLAUDE_CODE_USE_VERTEX=1, or CLAUDE_CODE_USE_FOUNDRY=1 alongside the respective cloud credentials.
Core Pattern: The query() Function
Every agent is built around query(), which returns an async iterator of streamed messages:
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", "Glob"],
permission_mode="acceptEdits",
),
):
if hasattr(message, "result"):
print(message.result)
asyncio.run(main())
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", "Glob"], permissionMode: "acceptEdits" }
})) {
if ("result" in message) console.log(message.result);
}
The loop ends when Claude finishes or hits an error. The SDK handles tool execution, context management, and retries internally.
Built-in Tools
Grant only the tools the agent actually needs — principle of least privilege:
| Tool | Purpose |
|---|---|
Read |
Read any file in the working directory |
Write |
Create new files |
Edit |
Make precise edits to existing files |
Bash |
Run terminal commands, scripts, git operations |
Glob |
Find files by pattern (**/*.ts, src/**/*.py) |
Grep |
Search file contents with regex |
WebSearch |
Search the web for current information |
WebFetch |
Fetch and parse web page content |
AskUserQuestion |
Ask the user clarifying questions |
Task |
Spawn subagents (required when using subagents) |
Recommended tool sets by use case
| Use case | Tools |
|---|---|
| Read-only analysis | Read, Glob, Grep |
| Code modification | Read, Edit, Write, Glob, Grep |
| Full automation | Read, Edit, Write, Bash, Glob, Grep |
| Web-augmented | Add WebSearch, WebFetch to any set |
| Subagent orchestration | Add Task to the parent agent's set |
Permission Modes
Set permissionMode / permission_mode in options:
| Mode | Behavior | Best for |
|---|---|---|
default |
Delegates unresolved requests to canUseTool callback |
Custom approval flows |
acceptEdits |
Auto-approves file edits and filesystem ops | Trusted dev workflows |
bypassPermissions |
Runs all tools without prompts | CI/CD pipelines |
plan |
No tool execution — planning only | Pre-review before changes |
Best practice: use acceptEdits for interactive development; use bypassPermissions only in isolated, sandboxed environments. Never use bypassPermissions in multi-tenant or user-facing applications.
Permission mode changes mid-session are supported — start restrictive, loosen after reviewing Claude's plan:
q = query(prompt="Refactor auth module", options=ClaudeAgentOptions(permission_mode="plan"))
await q.set_permission_mode("acceptEdits") # Switch after plan is approved
async for message in q:
...
Permission evaluation order
- Hooks — run first; can allow, deny, or pass through
- Permission rules — declarative allow/deny in
settings.json - Permission mode — global fallback setting
canUseToolcallback — runtime user approval (when mode isdefault)
Session Management
Capture session ID
session_id = None
async for message in query(prompt="Analyze auth module", options=ClaudeAgentOptions(...)):
if hasattr(message, "subtype") and message.subtype == "init":
session_id = message.data.get("session_id")
let sessionId: string | undefined;
for await (const message of query({ prompt: "Analyze auth module", options: { ... } })) {
if (message.type === "system" && message.subtype === "init") {
sessionId = message.session_id;
}
}
Resume a session
Pass resume / resume in options to continue with full prior context:
async for message in query(
prompt="Now find all callers of that function",
options=ClaudeAgentOptions(resume=session_id),
):
...
Fork a session
Set fork_session=True / forkSession: true to branch without modifying the original:
# Explore a different approach without losing the original session
async for message in query(
prompt="Redesign this as GraphQL instead",
options=ClaudeAgentOptions(resume=session_id, fork_session=True),
):
...
Forking preserves the original session; both branches can be resumed independently.
MCP Integration
Connect external services through the Model Context Protocol:
options = ClaudeAgentOptions(
mcp_servers={
"playwright": {"command": "npx", "args": ["@playwright/mcp@latest"]}
}
)
MCP tool names follow the pattern mcp__{server_name}__{tool_name}. List them explicitly in allowed_tools to restrict access:
allowed_tools=["mcp__playwright__browser_click", "mcp__playwright__browser_screenshot"]
Message Handling
Filter the stream for meaningful output. Raw messages include system init and internal state:
from claude_agent_sdk import AssistantMessage, ResultMessage
async for message in query(...):
if isinstance(message, AssistantMessage):
for block in message.content:
if hasattr(block, "text"):
print(block.text) # Claude's reasoning
elif hasattr(block, "name"):
print(f"Tool: {block.name}") # Tool being called
elif isinstance(message, ResultMessage):
print(f"Result: {message.subtype}") # "success" or "error"
for await (const message of query({ ... })) {
if (message.type === "assistant") {
for (const block of message.message.content) {
if ("text" in block) console.log(block.text);
else if ("name" in block) console.log(`Tool: ${block.name}`);
}
} else if (message.type === "result") {
console.log(`Result: ${message.subtype}`);
}
}
System Prompts
Provide a system_prompt / systemPrompt to give Claude a persona or project-specific context:
options = ClaudeAgentOptions(
system_prompt="You are a senior Python developer. Always follow PEP 8. Prefer explicit error handling over bare excepts.",
allowed_tools=["Read", "Edit", "Glob"],
permission_mode="acceptEdits",
)
Keep system prompts concise and focused on constraints the LLM doesn't already know.
Best Practices
Tool selection
- Grant only the tools the task requires — no
Bashfor read-only analysis - Verify tool set is sufficient before launching; a missing tool causes the agent to stall
- Include
Taskin the parent'sallowed_toolswhen defining subagents
Prompts
- Write prompts as specific task instructions, not open-ended descriptions
- Name files explicitly when they are the target (
"Review auth.py"vs"Review some code") - Use explicit subagent invocation when precision matters:
"Use the code-reviewer agent to...")
Error handling
- Always handle
ResultMessagewithsubtype == "error"— never assume success - Catch exceptions around the
async forloop, not inside it - Avoid bare
exceptin hook callbacks; a swallowed exception can silently halt the agent
Security
- Never use
bypassPermissionsin production systems with user-supplied prompts - Use
PreToolUsehooks to block access to sensitive paths (.env,/etc, secrets) - Restrict subagent tools — subagents do not inherit parent permissions automatically
Sessions
- Store session IDs persistently if the workflow spans multiple process runs
- Fork sessions when exploring alternative approaches to avoid losing a good baseline
- Subagent transcripts persist separately; clean up via
cleanupPeriodDayssetting
Performance
- Prefer streaming (
async for) over collecting all messages; it shows progress and allows early exit - Use subagents to parallelise independent tasks (security scan + style check simultaneously)
- Use
planmode first for expensive or irreversible operations — review before executing
Additional Resources
references/hooks.md— Lifecycle hooks: blocking tools, modifying inputs, audit loggingreferences/subagents.md— Defining, invoking, and resuming subagents; parallelisation patternsreferences/custom-tools.md— Building in-process MCP tools withcreateSdkMcpServer