The Claude Agent SDK makes Claude Code’s capabilities available as a library. The same tools Claude uses in your terminal — reading files, running commands, editing code — are available in a Python or TypeScript process you control.
The difference from prompt engineering or the standard Anthropic SDK: built-in tool execution. You don’t implement a tool loop. You describe a task, Claude runs it. Files get read. Code gets edited. Commands get executed. Results come back.
This guide builds from the basics to production patterns, covering every SDK feature with working examples.
Installation
# Python (requires 3.10+)
pip install claude-agent-sdk
# TypeScript
npm install @anthropic-ai/claude-agent-sdk
The TypeScript package bundles a Claude Code binary for your platform. You don’t need to install Claude Code separately.
Authentication
export ANTHROPIC_API_KEY=your-api-key-from-console.anthropic.com
The SDK also supports cloud providers:
# Amazon Bedrock
export CLAUDE_CODE_USE_BEDROCK=1
# Configure AWS credentials (IAM role, environment, or ~/.aws/credentials)
# Google Vertex AI
export CLAUDE_CODE_USE_VERTEX=1
# Configure Google Cloud credentials (gcloud auth, service account)
# Microsoft Azure AI Foundry
export CLAUDE_CODE_USE_FOUNDRY=1
# Configure Azure credentials
Note: claude.ai subscription login is not supported for API key authentication in Agent SDK applications. Use the API key or cloud provider credentials.
The Core Pattern: query()
Everything in the Agent SDK is built around query(). It takes a prompt and options, runs the agent loop, and yields messages as they’re produced.
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def main():
async for message in query(
prompt="Find all TODO comments in src/ and create a summary",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Glob", "Grep"]
),
):
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 all TODO comments in src/ and create a summary",
options: { allowedTools: ["Read", "Glob", "Grep"] }
})) {
if ("result" in message) console.log(message.result);
}
query() is an async generator. Messages stream out as the agent works. The final message with a result field contains Claude’s summary of what it did.
Built-In Tools Reference
Grant tools via allowedTools. Only listed tools are available. Claude can’t use a tool that isn’t in the list.
| Tool | What it does | Typical use |
|---|---|---|
Read | Read any file in the working directory | Codebase analysis, reading configs |
Write | Create new files | Generating reports, scaffolding |
Edit | Make precise edits to existing files | Bug fixes, refactoring |
Bash | Run terminal commands | Tests, git, builds, scripts |
Monitor | Watch a background script; react to each output line | Long-running process monitoring |
Glob | Find files by pattern (**/*.ts, src/**/*.py) | Scanning codebase structure |
Grep | Search file contents with regex | Finding usages, cross-references |
WebSearch | Search the web | Research, finding docs |
WebFetch | Fetch and parse web pages | Reading documentation, specs |
AskUserQuestion | Ask clarifying questions with multiple-choice options | Interactive agents |
For read-only analysis:
options=ClaudeAgentOptions(
allowed_tools=["Read", "Glob", "Grep", "WebSearch", "WebFetch"]
)
For full development work:
options=ClaudeAgentOptions(
allowed_tools=["Read", "Write", "Edit", "Bash", "Glob", "Grep"]
)
Hooks: Audit, Validate, Block
Hooks are callbacks that fire at specific points in the agent loop. Use them for audit logging, validating tool calls, blocking dangerous operations, or injecting context.
PostToolUse: Audit Logging
import asyncio
from datetime import datetime
from claude_agent_sdk import query, ClaudeAgentOptions, HookMatcher
async def log_file_change(input_data, tool_use_id, context):
file_path = input_data.get("tool_input", {}).get("file_path", "unknown")
with open("./audit.log", "a") as f:
f.write(f"{datetime.now().isoformat()}: EDIT {file_path} [{tool_use_id}]\n")
return {}
async def main():
async for message in query(
prompt="Refactor the auth module to use async/await",
options=ClaudeAgentOptions(
permission_mode="acceptEdits",
hooks={
"PostToolUse": [
HookMatcher(matcher="Edit|Write", hooks=[log_file_change])
]
},
),
):
if hasattr(message, "result"):
print(message.result)
asyncio.run(main())
import { query, HookCallback } from "@anthropic-ai/claude-agent-sdk";
import { appendFile } from "fs/promises";
const logFileChange: HookCallback = async (input) => {
const filePath = (input as any).tool_input?.file_path ?? "unknown";
const toolUseId = (input as any).tool_use_id ?? "unknown";
await appendFile("./audit.log",
`${new Date().toISOString()}: EDIT ${filePath} [${toolUseId}]\n`
);
return {};
};
for await (const message of query({
prompt: "Refactor the auth module to use async/await",
options: {
permissionMode: "acceptEdits",
hooks: {
PostToolUse: [{ matcher: "Edit|Write", hooks: [logFileChange] }]
}
}
})) {
if ("result" in message) console.log(message.result);
}
PreToolUse: Blocking Dangerous Operations
async def block_production_db(input_data, tool_use_id, context):
command = input_data.get("tool_input", {}).get("command", "")
if "prod-db.internal" in command or "production" in command.lower():
return {
"decision": "deny",
"reason": "Direct production database access blocked. Use the analytics replica."
}
return {}
options=ClaudeAgentOptions(
hooks={
"PreToolUse": [
HookMatcher(matcher="Bash", hooks=[block_production_db])
]
}
)
Available Hook Events
PreToolUse, PostToolUse, PostToolUseFailure, Stop, SessionStart, SessionEnd, UserPromptSubmit, SubagentStart, SubagentStop, TaskCreated, TaskCompleted, PreCompact, PostCompact, and more.
Subagents: Delegation
Define specialized agents and let Claude delegate to them.
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
async def main():
async for message in query(
prompt="""
Review the authentication module for security issues.
Use the security-reviewer agent for the deep analysis,
then use the doc-writer to document any findings.
""",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Glob", "Agent"],
agents={
"security-reviewer": AgentDefinition(
description="Expert security reviewer for code. Use when reviewing code for vulnerabilities, insecure patterns, or compliance issues.",
prompt="""You are a security-focused code reviewer.
Analyze code for: injection vulnerabilities, authentication bypasses,
insecure deserialization, secrets in code, and OWASP Top 10 issues.
Return structured findings with severity (critical/high/medium/low),
file location, and remediation steps.""",
tools=["Read", "Glob", "Grep"],
),
"doc-writer": AgentDefinition(
description="Technical documentation writer. Use when writing security reports, findings documentation, or README updates.",
prompt="Write clear, actionable documentation. Focus on severity, impact, and specific remediation steps.",
tools=["Read", "Write"],
),
},
),
):
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: `Review the authentication module for security issues.
Use the security-reviewer agent for the deep analysis,
then use the doc-writer to document any findings.`,
options: {
allowedTools: ["Read", "Glob", "Agent"],
agents: {
"security-reviewer": {
description: "Expert security reviewer for code. Use when reviewing code for vulnerabilities, insecure patterns, or compliance issues.",
prompt: `You are a security-focused code reviewer.
Analyze code for: injection vulnerabilities, authentication bypasses,
insecure deserialization, secrets in code, and OWASP Top 10 issues.
Return structured findings with severity and remediation steps.`,
tools: ["Read", "Glob", "Grep"]
},
"doc-writer": {
description: "Technical documentation writer. Use when writing security reports or findings documentation.",
prompt: "Write clear, actionable documentation with severity, impact, and remediation steps.",
tools: ["Read", "Write"]
}
}
}
})) {
if ("result" in message) console.log(message.result);
}
Include Agent in allowedTools to pre-approve subagent invocations. Without it, Claude will prompt before spawning each subagent.
Messages from subagents include parent_tool_use_id, letting you track which subagent produced each message.
Session Continuity
Capture a session ID and resume it later. Claude retains full context: files read, analysis done, conversation history.
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, SystemMessage, ResultMessage
async def main():
session_id = None
# First query: analyze the codebase
print("Phase 1: Analyzing authentication module...")
async for message in query(
prompt="Read and analyze the authentication module in src/auth/. Summarize the architecture and identify any concerns.",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Glob", "Grep"]
),
):
if isinstance(message, SystemMessage) and message.subtype == "init":
session_id = message.data["session_id"]
if isinstance(message, ResultMessage):
print(message.result)
# Second query: use the analysis from phase 1
print("\nPhase 2: Writing tests...")
async for message in query(
prompt="""Based on your analysis, write integration tests for the authentication module.
Focus on the concerns you identified. Create the test file at tests/auth/integration.test.ts""",
options=ClaudeAgentOptions(
resume=session_id,
allowed_tools=["Read", "Write", "Bash"]
),
):
if isinstance(message, ResultMessage):
print(message.result)
asyncio.run(main())
import { query, SystemMessage, ResultMessage } from "@anthropic-ai/claude-agent-sdk";
let sessionId: string | undefined;
// Phase 1: Analysis
for await (const message of query({
prompt: "Read and analyze the authentication module in src/auth/. Summarize architecture and concerns.",
options: { allowedTools: ["Read", "Glob", "Grep"] }
})) {
if (message.type === "system" && message.subtype === "init") {
sessionId = message.session_id;
}
if ("result" in message) console.log("Analysis:", message.result);
}
// Phase 2: Continue with full context
for await (const message of query({
prompt: "Based on your analysis, write integration tests for the authentication module.",
options: {
resume: sessionId,
allowedTools: ["Read", "Write", "Bash"]
}
})) {
if ("result" in message) console.log("Tests:", message.result);
}
Session state persists as JSONL on your filesystem. The resume option loads that state and continues the conversation.
MCP Integration
Connect Claude to external systems via MCP servers:
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def main():
async for message in query(
prompt="""
1. Check GitHub for issues labeled 'bug' opened in the last 7 days
2. For each bug, find the relevant code in this repository
3. Create a fix branch for the highest-priority bug
4. Implement a fix and open a draft PR
""",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Edit", "Bash", "Glob", "Grep"],
mcp_servers={
"github": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/",
"headers": {
"Authorization": f"Bearer {os.environ['GITHUB_PAT']}"
}
}
}
),
):
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: "Check GitHub for open bugs, find the relevant code, and open a fix PR for the highest-priority one.",
options: {
allowedTools: ["Read", "Edit", "Bash", "Glob"],
mcpServers: {
github: {
type: "http",
url: "https://api.githubcopilot.com/mcp/",
headers: {
Authorization: `Bearer ${process.env.GITHUB_PAT}`
}
}
}
}
})) {
if ("result" in message) console.log(message.result);
}
For Playwright browser automation:
options=ClaudeAgentOptions(
mcp_servers={
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest"]
}
}
)
Permission Modes in the SDK
The same permission modes from Claude Code CLI apply:
from claude_agent_sdk import ClaudeAgentOptions
# Read-only analysis agent
options = ClaudeAgentOptions(
permission_mode="plan" # Reads only, no modifications
)
# Auto-approve file edits
options = ClaudeAgentOptions(
permission_mode="acceptEdits" # File edits auto-approved
)
# Full automation (CI/CD in isolated environments)
options = ClaudeAgentOptions(
permission_mode="bypassPermissions" # No prompts at all
)
# Explicit allowlist only
options = ClaudeAgentOptions(
permission_mode="dontAsk",
allowed_tools=["Read", "Bash(npm test *)"] # Only these tools, no prompts
)
Handling User Input
The AskUserQuestion tool lets Claude ask clarifying questions during execution:
async def handle_user_input(prompt: str, choices: list[str] | None = None) -> str:
"""Called when Claude needs user input"""
if choices:
for i, choice in enumerate(choices):
print(f" {i + 1}. {choice}")
answer = input(f"{prompt} (enter number): ")
return choices[int(answer) - 1]
return input(f"{prompt}: ")
async def main():
async for message in query(
prompt="Review this codebase and propose a refactoring plan. Ask me about priorities before starting.",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Glob", "Grep", "AskUserQuestion"],
on_user_question=handle_user_input
),
):
if hasattr(message, "result"):
print(message.result)
CLAUDE.md and Skills in Agent SDK Sessions
The SDK loads Claude Code configuration from .claude/ by default:
./CLAUDE.md # Project instructions Claude reads
./.claude/CLAUDE.md # Alternative location
./.claude/skills/*/SKILL.md # Skills Claude can use
./.claude/commands/*.md # Legacy slash commands
To restrict which configuration sources load:
options=ClaudeAgentOptions(
setting_sources=["project"] # Only load project-level config, not user-level
)
To pass a custom system prompt:
options=ClaudeAgentOptions(
system_prompt="You are a Python expert. This is a Python project using FastAPI."
)
Production Patterns
Bug-Fixing Pipeline
import asyncio
import json
from pathlib import Path
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage, SystemMessage
async def fix_bug(issue_description: str, repo_path: str) -> dict:
"""Run Claude on a bug description, return fix details."""
session_id = None
result = None
edits_made = []
async def track_edit(input_data, tool_use_id, context):
if "file_path" in input_data.get("tool_input", {}):
edits_made.append(input_data["tool_input"]["file_path"])
return {}
async for message in query(
prompt=f"""
Bug report: {issue_description}
1. Analyze the bug and find the root cause
2. Implement a minimal fix
3. Add a test that would have caught this bug
4. Explain what you changed and why
""",
options=ClaudeAgentOptions(
cwd=repo_path,
permission_mode="acceptEdits",
allowed_tools=["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
hooks={
"PostToolUse": [
{"matcher": "Edit|Write", "hooks": [track_edit]}
]
}
),
):
if isinstance(message, SystemMessage) and message.subtype == "init":
session_id = message.data["session_id"]
if isinstance(message, ResultMessage):
result = message.result
return {
"session_id": session_id,
"summary": result,
"files_changed": edits_made
}
async def main():
fix = await fix_bug(
issue_description="TypeError in auth.handleCallback() when refresh token is expired",
repo_path="/Users/me/projects/my-api"
)
print(f"Fix summary: {fix['summary']}")
print(f"Files changed: {fix['files_changed']}")
asyncio.run(main())
Parallel Code Review
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def review_file(file_path: str) -> dict:
"""Review a single file for quality, security, and style."""
result = None
async for message in query(
prompt=f"Review {file_path} for security issues, code quality, and adherence to best practices. Return structured findings.",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Grep"],
permission_mode="plan"
),
):
if isinstance(message, ResultMessage):
result = message.result
return {"file": file_path, "findings": result}
async def review_pr_files(changed_files: list[str]) -> list[dict]:
"""Review multiple files in parallel."""
tasks = [review_file(f) for f in changed_files]
return await asyncio.gather(*tasks)
async def main():
files = [
"src/auth/handler.py",
"src/api/routes.py",
"src/db/queries.py"
]
results = await review_pr_files(files)
for r in results:
print(f"\n{r['file']}:\n{r['findings']}\n")
asyncio.run(main())
CI/CD Integration Script
#!/usr/bin/env python3
"""
Run in CI on pull_request events.
Checks code quality and posts a review comment.
"""
import asyncio
import os
import subprocess
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def run_pr_review(pr_number: str) -> str:
result = None
async for message in query(
prompt=f"""
Review PR #{pr_number} against our coding standards in CLAUDE.md.
Check:
1. Input validation on all new endpoints
2. Proper error handling (no bare except/catch)
3. No hardcoded secrets or credentials
4. Tests for new functionality
5. No TODOs left uncommented
Return a structured review with pass/fail for each check
and specific file:line references for any issues.
""",
options=ClaudeAgentOptions(
permission_mode="plan", # Read-only, no modifications
allowed_tools=["Read", "Glob", "Grep", "Bash"]
),
):
if isinstance(message, ResultMessage):
result = message.result
return result or "Review failed to produce output"
async def main():
pr_number = os.environ.get("PR_NUMBER", "")
if not pr_number:
print("PR_NUMBER not set")
raise SystemExit(1)
review = await run_pr_review(pr_number)
# Post the review as a PR comment using GitHub CLI
subprocess.run(
["gh", "pr", "comment", pr_number, "--body", review],
check=True
)
print("Review posted successfully")
asyncio.run(main())
Message Types
Messages from query() have different shapes depending on what they represent:
from claude_agent_sdk import (
SystemMessage, # Session initialization, subagent events
AssistantMessage, # Claude's text output
ToolUseMessage, # Claude is using a tool
ToolResultMessage, # Tool execution result
ResultMessage, # Final result (has .result field)
UserMessage # User turn messages
)
async for message in query(prompt=..., options=...):
if isinstance(message, SystemMessage):
if message.subtype == "init":
session_id = message.data["session_id"]
elif isinstance(message, AssistantMessage):
# Claude's thinking/narration
for block in message.content:
if hasattr(block, "text"):
print("Claude:", block.text)
elif isinstance(message, ResultMessage):
# Final summary
print("Result:", message.result)
print("Cost:", message.cost_usd)
print("Duration:", message.duration_seconds)
Agent SDK vs. Claude Code CLI vs. Managed Agents
| Agent SDK | Claude Code CLI | Managed Agents | |
|---|---|---|---|
| Interface | Python/TypeScript library | Interactive terminal | REST API |
| Runs in | Your process | Your terminal | Anthropic infrastructure |
| File access | Your filesystem | Your filesystem | Managed sandbox |
| Custom tools | Hooks + MCP | Hooks + MCP | REST callbacks |
| Session state | JSONL on disk | Interactive | Anthropic-hosted |
| Best for | CI/CD, applications, automation | Daily development | Production agents without managing infra |
A common path: prototype locally with the Agent SDK, move to Managed Agents for production when you want Anthropic to manage the infrastructure.
Important Note on Credit Usage
Starting June 15, 2026, Agent SDK and claude -p usage on subscription plans draws from a separate monthly Agent SDK credit, distinct from interactive usage limits. Check Anthropic’s support docs for current details.
Internal Links
- Claude Code Multi-Agent Orchestration Patterns — Multi-agent patterns in interactive sessions (complementary to Agent SDK subagents)
- Claude Code Hooks Complete Reference — Full hook system reference (the Agent SDK uses the same hook events)
- Claude Code MCP GitHub Sentry PostgreSQL Setup — MCP server setup for use with the Agent SDK
- Claude Code Permission Modes Complete Guide — Permission modes that apply in both CLI and SDK
- Claude Code GitHub Actions Complete Guide — GitHub Actions is built on the Agent SDK