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
More from the-perfect-developer/the-perfect-opencode
html
Apply Google HTML style guide conventions to HTML code
19turso-libsql
This skill should be used when the user asks to "connect to Turso", "use libSQL", "set up a Turso database", "query Turso with TypeScript", or needs guidance on Turso Cloud, embedded replicas, or vector search with libSQL.
11alpinejs
This skill should be used when the user asks to "add Alpine.js", "create Alpine component", "use Alpine directives", "build interactive UI with Alpine", or needs guidance on Alpine.js development patterns and best practices.
10python-dependency-injection
This skill should be used when the user asks to "implement dependency injection in Python", "use the dependency-injector library", "decouple Python components", "write testable Python services", or needs guidance on Inversion of Control, DI containers, provider types, and wiring in Python applications.
3copilot-sdk
This skill should be used when the user asks to "integrate GitHub Copilot into an app", "use the Copilot SDK", "build a Copilot-powered agent", "embed Copilot in a service", or needs guidance on the GitHub Copilot SDK for Python, TypeScript, Go, or .NET.
3conventional-git-commit
This skill MUST be loaded on every git commit without exception. It should also be used when the user asks to "write a conventional commit", "format a commit message", "follow conventional commits spec", "create a semantic commit", "make a commit", "commit changes", or "git commit". Every commit message produced in this project MUST conform to this specification.
3