357 lines
13 KiB
Markdown
357 lines
13 KiB
Markdown
---
|
|
title: "Agent SDK Python API Reference"
|
|
aliases: [python-sdk-reference, claude-agent-sdk-python]
|
|
tags: [agent-sdk, python, api-reference, anthropic, claude]
|
|
sources: [raw/Agent SDK reference - Python.md]
|
|
created: 2026-04-17
|
|
updated: 2026-04-17
|
|
---
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
pip install claude-agent-sdk
|
|
```
|
|
|
|
## query() vs ClaudeSDKClient
|
|
|
|
Two interaction modes — choose based on whether you need conversation continuity.
|
|
|
|
| Feature | `query()` | `ClaudeSDKClient` |
|
|
|---------|-----------|-------------------|
|
|
| Session | New each call | Reuses same session |
|
|
| Conversation | Single exchange | Multi-turn context |
|
|
| Interrupts | No | Yes |
|
|
| Hooks | Yes | Yes |
|
|
| Custom Tools | Yes | Yes |
|
|
| Use Case | One-off tasks | Continuous conversations |
|
|
|
|
**Use `query()`** for independent tasks, automation scripts, no history needed.
|
|
**Use `ClaudeSDKClient`** for chat interfaces, follow-up questions, response-driven logic.
|
|
|
|
---
|
|
|
|
## Core Functions
|
|
|
|
### query()
|
|
|
|
```python
|
|
async def query(
|
|
*,
|
|
prompt: str | AsyncIterable[dict[str, Any]],
|
|
options: ClaudeAgentOptions | None = None,
|
|
transport: Transport | None = None
|
|
) -> AsyncIterator[Message]
|
|
```
|
|
|
|
Creates a new session per call. Returns an `AsyncIterator[Message]`.
|
|
|
|
```python
|
|
async for message in query(prompt="Create a Python web server", options=options):
|
|
print(message)
|
|
```
|
|
|
|
### tool() decorator
|
|
|
|
Defines MCP tools with type safety.
|
|
|
|
```python
|
|
@tool("greet", "Greet a user", {"name": str})
|
|
async def greet(args: dict[str, Any]) -> dict[str, Any]:
|
|
return {"content": [{"type": "text", "text": f"Hello, {args['name']}!"}]}
|
|
```
|
|
|
|
Input schema options:
|
|
- Simple: `{"text": str, "count": int}`
|
|
- Full JSON Schema: `{"type": "object", "properties": {...}, "required": [...]}`
|
|
|
|
**ToolAnnotations** (optional hints, not security decisions):
|
|
- `readOnlyHint` — tool doesn't modify environment
|
|
- `destructiveHint` — tool may perform destructive updates
|
|
- `idempotentHint` — repeated calls have no extra effect
|
|
- `openWorldHint` — tool interacts with external entities
|
|
|
|
### create_sdk_mcp_server()
|
|
|
|
```python
|
|
def create_sdk_mcp_server(
|
|
name: str,
|
|
version: str = "1.0.0",
|
|
tools: list[SdkMcpTool[Any]] | None = None
|
|
) -> McpSdkServerConfig
|
|
```
|
|
|
|
Creates an in-process MCP server. Pass result to `ClaudeAgentOptions.mcp_servers`.
|
|
|
|
```python
|
|
calculator = create_sdk_mcp_server(name="calculator", tools=[add, multiply])
|
|
options = ClaudeAgentOptions(
|
|
mcp_servers={"calc": calculator},
|
|
allowed_tools=["mcp__calc__add", "mcp__calc__multiply"],
|
|
)
|
|
```
|
|
|
|
### Session Management (synchronous)
|
|
|
|
| Function | Description |
|
|
|---------|-------------|
|
|
| `list_sessions(directory, limit, include_worktrees)` | List past sessions with metadata |
|
|
| `get_session_messages(session_id, directory, limit, offset)` | Get messages from a past session |
|
|
| `get_session_info(session_id, directory)` | Metadata for a single session by ID |
|
|
| `rename_session(session_id, title, directory)` | Set custom title; most recent wins |
|
|
| `tag_session(session_id, tag, directory)` | Tag a session; pass `None` to clear |
|
|
|
|
`SDKSessionInfo` fields: `session_id`, `summary`, `last_modified`, `custom_title`, `first_prompt`, `git_branch`, `cwd`, `tag`, `created_at`.
|
|
|
|
---
|
|
|
|
## ClaudeSDKClient
|
|
|
|
Maintains conversation context across multiple exchanges.
|
|
|
|
```python
|
|
async with ClaudeSDKClient() as client:
|
|
await client.query("What's the capital of France?")
|
|
async for message in client.receive_response():
|
|
...
|
|
await client.query("What's the population of that city?") # retains context
|
|
async for message in client.receive_response():
|
|
...
|
|
```
|
|
|
|
### Key Methods
|
|
|
|
| Method | Description |
|
|
|--------|-------------|
|
|
| `connect(prompt)` | Connect with optional initial prompt |
|
|
| `query(prompt, session_id)` | Send request in streaming mode |
|
|
| `receive_messages()` | All messages as async iterator |
|
|
| `receive_response()` | Messages until (and including) `ResultMessage` |
|
|
| `interrupt()` | Send stop signal (streaming mode only) |
|
|
| `set_permission_mode(mode)` | Change permission mode mid-session |
|
|
| `set_model(model)` | Change model; `None` resets to default |
|
|
| `rewind_files(user_message_id)` | Restore files to state at message (requires `enable_file_checkpointing=True`) |
|
|
| `get_mcp_status()` | Status of all MCP servers |
|
|
| `reconnect_mcp_server(name)` | Retry failed MCP server |
|
|
| `toggle_mcp_server(name, enabled)` | Enable/disable MCP server mid-session |
|
|
| `stop_task(task_id)` | Stop a background task |
|
|
| `disconnect()` | End session |
|
|
|
|
### Interrupt pattern
|
|
|
|
`interrupt()` sends a stop signal but **does not clear the buffer**. You must drain the interrupted task's messages (including its `ResultMessage` with `subtype="error_during_execution"`) before reading the new response.
|
|
|
|
```python
|
|
await client.interrupt()
|
|
async for message in client.receive_response(): # drain interrupted task
|
|
if isinstance(message, ResultMessage): break
|
|
await client.query("Just say hello instead")
|
|
async for message in client.receive_response(): # now get new response
|
|
...
|
|
```
|
|
|
|
---
|
|
|
|
## ClaudeAgentOptions
|
|
|
|
Key configuration fields:
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `allowed_tools` | `list[str]` | Auto-approve these tools (doesn't restrict others) |
|
|
| `disallowed_tools` | `list[str]` | Always deny; overrides `allowed_tools` and `bypassPermissions` |
|
|
| `permission_mode` | `PermissionMode` | `"default"`, `"acceptEdits"`, `"plan"`, `"dontAsk"`, `"bypassPermissions"` |
|
|
| `system_prompt` | `str \| SystemPromptPreset` | Custom string or `{"type":"preset","preset":"claude_code"}` |
|
|
| `mcp_servers` | `dict[str, McpServerConfig]` | MCP server configs or path to config file |
|
|
| `cwd` | `str \| Path` | Working directory |
|
|
| `model` | `str` | Claude model to use |
|
|
| `max_turns` | `int` | Max agentic turns (tool-use round trips) |
|
|
| `max_budget_usd` | `float` | Stop when cost estimate reaches this value |
|
|
| `can_use_tool` | `CanUseTool` | Custom permission callback |
|
|
| `hooks` | `dict[HookEvent, list[HookMatcher]]` | Hook configurations |
|
|
| `thinking` | `ThinkingConfig` | `{"type":"adaptive"}`, `{"type":"enabled","budget_tokens":N}`, `{"type":"disabled"}` |
|
|
| `effort` | `Literal["low","medium","high","xhigh","max"]` | Thinking depth |
|
|
| `enable_file_checkpointing` | `bool` | Enable file rewinding |
|
|
| `setting_sources` | `list[SettingSource]` | Which filesystem settings to load (`"user"`, `"project"`, `"local"`); `[]` disables all |
|
|
| `agents` | `dict[str, AgentDefinition]` | Programmatic subagents |
|
|
| `sandbox` | `SandboxSettings` | Sandbox configuration |
|
|
| `output_format` | `dict` | `{"type":"json_schema","schema":{...}}` for structured output |
|
|
| `fork_session` | `bool` | When resuming, fork instead of continuing original |
|
|
| `include_partial_messages` | `bool` | Emit `StreamEvent` for partial updates |
|
|
|
|
### SettingSource / settings_sources
|
|
|
|
```python
|
|
# Disable all filesystem settings (CI/SDK-only apps)
|
|
ClaudeAgentOptions(setting_sources=[])
|
|
|
|
# Load only shared team settings
|
|
ClaudeAgentOptions(setting_sources=["project"])
|
|
|
|
# Load project settings to include CLAUDE.md
|
|
ClaudeAgentOptions(
|
|
system_prompt={"type":"preset","preset":"claude_code"},
|
|
setting_sources=["project"]
|
|
)
|
|
```
|
|
|
|
Settings precedence: local > project > user. Programmatic options override filesystem settings. Managed policy settings win over everything.
|
|
|
|
---
|
|
|
|
## Permission System
|
|
|
|
### CanUseTool callback
|
|
|
|
```python
|
|
async def custom_permission(
|
|
tool_name: str, input_data: dict, context: ToolPermissionContext
|
|
) -> PermissionResultAllow | PermissionResultDeny:
|
|
if tool_name == "Write" and "config" in input_data.get("file_path", ""):
|
|
return PermissionResultAllow(updated_input={**input_data, "file_path": f"./sandbox/{input_data['file_path']}"})
|
|
return PermissionResultAllow(updated_input=input_data)
|
|
```
|
|
|
|
- `PermissionResultAllow(updated_input=...)` — allow, optionally modify input
|
|
- `PermissionResultDeny(message="...", interrupt=True)` — deny, optionally interrupt
|
|
|
|
---
|
|
|
|
## Message Types
|
|
|
|
```
|
|
Message = UserMessage | AssistantMessage | SystemMessage | ResultMessage | StreamEvent | RateLimitEvent
|
|
ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock
|
|
```
|
|
|
|
### ResultMessage key fields
|
|
|
|
- `subtype` — `"success"` or `"error_during_execution"`
|
|
- `total_cost_usd` — client-side cost estimate
|
|
- `usage` — `{input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens}`
|
|
- `model_usage` — per-model breakdown (camelCase keys: `inputTokens`, `costUSD`, etc.)
|
|
- `session_id`, `num_turns`, `duration_ms`
|
|
|
|
### Background task messages
|
|
|
|
- `TaskStartedMessage` — emitted when background task starts; `task_type`: `"local_bash"`, `"local_agent"`, `"remote_agent"`
|
|
- `TaskProgressMessage` — periodic updates with `TaskUsage` (tokens, tool_uses, duration_ms)
|
|
- `TaskNotificationMessage` — completion; `status`: `"completed"`, `"failed"`, `"stopped"`
|
|
|
|
### RateLimitEvent
|
|
|
|
`status`: `"allowed"`, `"allowed_warning"`, `"rejected"`. Use to warn users or back off.
|
|
|
|
---
|
|
|
|
## Hook System
|
|
|
|
```python
|
|
HookEvent = Literal[
|
|
"PreToolUse", "PostToolUse", "PostToolUseFailure",
|
|
"UserPromptSubmit", "Stop", "SubagentStop", "PreCompact",
|
|
"Notification", "SubagentStart", "PermissionRequest"
|
|
]
|
|
```
|
|
|
|
```python
|
|
options = ClaudeAgentOptions(
|
|
hooks={
|
|
"PreToolUse": [
|
|
HookMatcher(matcher="Bash", hooks=[validate_bash], timeout=120),
|
|
HookMatcher(hooks=[log_all_tools]), # matches all tools
|
|
]
|
|
}
|
|
)
|
|
```
|
|
|
|
Hook callback signature: `async def hook(input_data, tool_use_id, context) -> HookJSONOutput`
|
|
|
|
Return values:
|
|
- `{}` — no-op
|
|
- `{"continue_": False, "stopReason": "..."}` — stop execution
|
|
- `{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", ...}}`
|
|
- `{"async_": True, "asyncTimeout": 5000}` — defer execution
|
|
|
|
Note: Python SDK hooks don't yet support `SessionStart`, `SessionEnd`, `Setup` (TypeScript only).
|
|
|
|
---
|
|
|
|
## Sandbox Settings
|
|
|
|
```python
|
|
SandboxSettings = {
|
|
"enabled": True,
|
|
"autoAllowBashIfSandboxed": True,
|
|
"excludedCommands": ["docker"], # always bypass sandbox, no model involvement
|
|
"allowUnsandboxedCommands": True, # model can request bypass via dangerouslyDisableSandbox
|
|
"network": {
|
|
"allowLocalBinding": True,
|
|
"allowUnixSockets": ["/var/run/docker.sock"], # ⚠️ grants full Docker/host access
|
|
}
|
|
}
|
|
```
|
|
|
|
> **Security**: `bypassPermissions` + `allowUnsandboxedCommands=True` lets the model escape the sandbox silently.
|
|
|
|
---
|
|
|
|
## Built-in Tool Reference (Input/Output shapes)
|
|
|
|
| Tool | Key Input | Key Output |
|
|
|------|-----------|------------|
|
|
| `Bash` | `command`, `timeout`, `run_in_background` | `output`, `exitCode`, `shellId` |
|
|
| `Read` | `file_path`, `offset`, `limit` | `content`, `total_lines` |
|
|
| `Write` | `file_path`, `content` | `bytes_written` |
|
|
| `Edit` | `file_path`, `old_string`, `new_string`, `replace_all` | `replacements` |
|
|
| `Glob` | `pattern`, `path` | `matches`, `count` |
|
|
| `Grep` | `pattern`, `path`, `glob`, `output_mode` | `matches` or `files` |
|
|
| `WebFetch` | `url`, `prompt` | `response`, `status_code` |
|
|
| `WebSearch` | `query`, `allowed_domains` | `results`, `total_results` |
|
|
| `Agent` | `description`, `prompt`, `subagent_type` | `result`, `usage`, `total_cost_usd` |
|
|
| `Monitor` | `command`, `description`, `timeout_ms`, `persistent` | `taskId` |
|
|
| `TodoWrite` | `todos[]` with `content`, `status` | `stats` |
|
|
|
|
---
|
|
|
|
## Type System Notes
|
|
|
|
- `@dataclass` types (e.g. `ResultMessage`, `TextBlock`) → attribute access: `msg.result`
|
|
- `TypedDict` types (e.g. `ThinkingConfigEnabled`, `McpStdioServerConfig`) → dict access: `config["budget_tokens"]`
|
|
|
|
---
|
|
|
|
## Error Types
|
|
|
|
| Exception | When |
|
|
|-----------|------|
|
|
| `CLINotFoundError` | Claude Code CLI not installed |
|
|
| `CLIConnectionError` | Connection to Claude Code failed |
|
|
| `ProcessError` | Claude Code process failed (`exit_code`, `stderr` attrs) |
|
|
| `CLIJSONDecodeError` | JSON parsing failed (`line`, `original_error` attrs) |
|
|
|
|
---
|
|
|
|
## Key Takeaways
|
|
|
|
- `query()` = stateless one-shot; `ClaudeSDKClient` = stateful multi-turn conversation
|
|
- After `interrupt()`, drain the buffer with `receive_response()` before sending a new query
|
|
- `disallowed_tools` overrides everything including `bypassPermissions`
|
|
- `allowed_tools` auto-approves but does NOT restrict — use `disallowed_tools` to block
|
|
- `setting_sources=[]` disables all filesystem settings (good for CI and SDK-only apps)
|
|
- `ThinkingConfig` and `McpServerConfig` variants are `TypedDict` (dict at runtime, not objects)
|
|
- `SdkBeta: "context-1m-2025-08-07"` is retired after 2026-04-30; Claude Sonnet/Opus 4.6+ have 1M context natively
|
|
- Sandbox + `allowUnixSockets` for Docker socket grants full host access — high risk
|
|
- Hook callbacks use `continue_` and `async_` (with underscores) in Python; SDK converts to `continue`/`async` for CLI
|
|
|
|
---
|
|
|
|
## Sources
|
|
|
|
- [raw/Agent SDK reference - Python.md](raw/Agent%20SDK%20reference%20-%20Python.md)
|
|
|
|
## Related
|
|
|
|
- [[wiki/agent-sdk/overview|Agent SDK Overview]]
|
|
- [[wiki/tech-patterns/ai-patterns|AI Patterns]]
|
|
- [[wiki/architecture/multi-agent-ai|Multi-Agent AI Architecture]]
|