claude-agent-sdk

Expert guide for building AI agents with the Claude Agent SDK (Python & TypeScript). Use this skill PROACTIVELY whenever: - Building or configuring AI agents with `claude_agent_sdk` or `@anthropic-ai/claude-agent-sdk` - Working with `query()`, `ClaudeAgentOptions`, `AgentDefinition`, hooks, sessions, subagents, MCP, or custom tools - User asks how to automate tasks, build pipelines, or orchestrate multi-agent workflows with Claude - Implementing permission controls, `canUseTool` callbacks, or tool restrictions - Connecting external services (databases, APIs, browsers) to a Claude agent via MCP - Debugging or optimizing agent behavior, streaming, sessions, or hooks - Any mention of "Claude agent", "agent SDK", "agentic loop", or autonomous Claude workflows Covers the full SDK: query API, built-in tools, hooks lifecycle, session management, subagents, MCP servers, custom in-process tools, permission modes, user input flows, structured outputs, thinking config, effort levels, V2 preview API, session discovery, error handling, and authentication.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "claude-agent-sdk" with this command: npx skills add leonvillamayor/skill-claude-agent/leonvillamayor-skill-claude-agent-claude-agent-sdk

Claude Agent SDK — Complete Guide

The Claude Agent SDK lets you build AI agents that autonomously read files, run commands, search the web, edit code, and more — using the same tools and agent loop that power Claude Code.

Languages: Python (claude-agent-sdk) and TypeScript (@anthropic-ai/claude-agent-sdk)

Quick Reference

GoalGo to
First agentSetup & Installation
Configure tools/permissionsCore Options (ClaudeAgentOptions)
React to tool callsHooks
Multi-turn memorySessions
Delegate to specialistsSubagents
Connect external APIs/DBsMCP Servers
Build in-process toolsCustom Tools
Load skills & pluginsSkills & Plugins
Control tool approvalsPermissions & User Input
Customize system promptSystem Prompts & CLAUDE.md
Track costs & usageCost Tracking
Undo file changesFile Checkpointing
Interrupt / stop agentInterrupt & Stop Reasons
Multi-turn clientClaudeSDKClient
Structured outputStructured Outputs
Thinking & effortThinking & Effort
Sandbox & secure deploySandbox & Secure Deployment
SDK internals & IPCSDK Internal Architecture
Deploy to productionAuthentication & Hosting
Use Pro/Max subscriptionUsing Pro/Max Subscription with the SDK
Session discoverySession Discovery
V2 Preview API (TS)TypeScript V2 Preview
Query object methods (TS)Query Object Methods (TS)
Error handlingError Types
Advanced referencereferences/ directory

Setup & Installation

# TypeScript
npm install @anthropic-ai/claude-agent-sdk

# Python (pip)
pip install claude-agent-sdk

# Python (uv — recommended)
uv add claude-agent-sdk

Set your API key:

export ANTHROPIC_API_KEY=your-api-key

Minimal working agent:

# Python
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions

async def main():
    async for message in query(
        prompt="What files are in this directory?",
        options=ClaudeAgentOptions(allowed_tools=["Bash", "Glob"]),
    ):
        if hasattr(message, "result"):
            print(message.result)

asyncio.run(main())
// TypeScript
import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "What files are in this directory?",
  options: { allowedTools: ["Bash", "Glob"] }
})) {
  if ("result" in message) console.log(message.result);
}

Core Options (ClaudeAgentOptions)

All options for query() — Python uses snake_case, TypeScript uses camelCase.

Option (Python / TS)TypeDescription
allowed_tools / allowedToolsstring[]Whitelist of tools Claude can use
disallowed_tools / disallowedToolsstring[]Blacklist of tools
toolsstring[] | ToolsPresetAlternative to allowed_tools; also accepts presets
permission_mode / permissionModestring"default", "acceptEdits", "bypassPermissions", "plan", "dontAsk"
system_prompt / systemPromptstring | objectCustom string or preset object (see System Prompts)
modelstringModel ID or shorthand ("sonnet", "opus", "haiku")
fallback_model / fallbackModelstringFallback model if primary is unavailable
max_turns / maxTurnsnumberMaximum agentic turns
max_budget_usd / maxBudgetUsdfloatMaximum spend in USD per query (triggers error_max_budget_usd on exceed)
thinkingobjectThinking config: {type:"adaptive"}, {type:"enabled",budget_tokens:N}, {type:"disabled"}
effortstringThinking depth: "low", "medium", "high", "max"
max_thinking_tokens / maxThinkingTokensnumberDeprecated — use thinking instead
output_format / outputFormatobjectStructured output schema: {type:"json_schema", schema:{...}}
resumestringSession ID to resume
fork_session / forkSessionbooleanFork session instead of continuing
continue_conversation / continuebooleanContinue most recent conversation
mcp_servers / mcpServersobject | stringMCP server configurations (inline object or path to .mcp.json)
agentsobjectSubagent definitions (keyed by name)
hooksobjectHook event callbacks
can_use_tool / canUseToolfunctionCallback for runtime tool approval
setting_sources / settingSourcesstring[]Load from filesystem (["user", "project", "local"])
cwdstringWorking directory for the agent
envobjectEnvironment variables to pass
pluginsarrayProgrammatic plugins (slash commands, agents, MCP)
sandboxobjectSandbox settings (see Sandbox)
enable_file_checkpointing / enableFileCheckpointingbooleanEnable file change tracking for rewind
extra_args / extraArgsobjectAdditional CLI arguments to pass
betasstring[]Beta features (e.g., ["context-1m-2025-08-07"] for 1M context window)
add_dirs / additionalDirectoriesstring[]Additional directories Claude can access
userstringUser identifier (for logging/tracking)
cli_path / cliPathstringCustom path to Claude CLI binary
include_partial_messages / includePartialMessagesbooleanInclude partial streaming messages
stderrfunctionCustom stderr handler callback

TypeScript-only options:

Option (TS only)TypeDescription
sessionIdstringUse a specific UUID for the session
persistSessionbooleanDisable disk persistence (false to skip saving)
resumeSessionAtstringResume at a specific message UUID
promptSuggestionsbooleanEnable SDKPromptSuggestionMessage after each turn
abortControllerAbortControllerFor cancelling operations externally
agentstringAgent name for the main thread
debug / debugFileboolean / stringEnable debug mode / write debug logs to file
spawnClaudeCodeProcessfunctionCustom process spawning (VMs, containers, remote)
allowDangerouslySkipPermissionsbooleanRequired when using bypassPermissions
strictMcpConfigbooleanEnforce strict MCP server validation
executable / executableArgsstring / string[]JS runtime ("bun", "deno", "node") and args

Built-in Tools Reference

ToolPurpose
ReadRead any file (text, image, PDF, Jupyter notebooks)
WriteCreate new files
EditPrecise string replacements in existing files
BashRun terminal commands, scripts, git (supports background execution)
GlobFind files by pattern (**/*.ts)
GrepSearch file contents with regex (ripgrep-based)
WebSearchSearch the web
WebFetchFetch and parse web pages
AskUserQuestionAsk user clarifying questions (multiple choice)
TaskInvoke subagents (required when using agents)
TaskOutputRetrieve output from background tasks
TaskStopStop a running background task
SkillInvoke installed skills (requires setting_sources)
TodoWriteCreate/manage structured task lists for progress tracking
NotebookEditEdit Jupyter notebook cells (.ipynb files)
ConfigGet/set agent configuration values
ExitPlanModeExit planning mode with optional allowed prompts
EnterWorktreeCreate and enter a git worktree for isolated work
ListMcpResourcesList available resources from configured MCP servers
ReadMcpResourceRead a specific resource from an MCP server by URI

Common tool combinations:

  • Read-only: ["Read", "Glob", "Grep"]
  • Code editing: ["Read", "Edit", "Write", "Glob", "Grep"]
  • Full automation: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"]
  • Web research: ["WebSearch", "WebFetch", "Read"]
  • With subagents: add "Task" to any combination
  • With skills: add "Skill" and configure setting_sources

Message Types

The query() async iterator yields typed messages:

# Python — check message type
from claude_agent_sdk import AssistantMessage, ResultMessage, SystemMessage

async for message in query(prompt="...", options=...):
    if isinstance(message, SystemMessage) and message.subtype == "init":
        session_id = message.data.get("session_id")
    elif 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 call
    elif isinstance(message, ResultMessage):
        print(f"Done ({message.subtype}): {message.result}")
// TypeScript — check message type
for await (const message of query({ prompt: "..." })) {
  if (message.type === "system" && message.subtype === "init") {
    const sessionId = message.session_id;
  } else 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(`Done (${message.subtype}): ${message.result}`);
  }
}

Result subtypes: "success" | "error_during_execution" | "error_max_turns" | "error_max_budget_usd" | "error_max_structured_output_retries"

ResultMessage extended fields:

  • result — Final text output
  • stop_reason"end_turn" or "tool_use"
  • num_turns, duration_ms, total_cost_usd
  • session_id — For session resumption
  • structured_output — Parsed JSON when using output_format
  • permission_denials — Array of {tool_name, tool_use_id, tool_input} for denied tools
  • modelUsage — Per-model usage breakdown: {inputTokens, outputTokens, cacheReadInputTokens, cacheCreationInputTokens, webSearchRequests, costUSD, contextWindow, maxOutputTokens}

Additional Message Types (TypeScript)

TypeScript exposes additional message types beyond the core four:

Message TypeDescription
SDKPartialAssistantMessagePartial streaming message (requires includePartialMessages)
SDKStatusMessageStatus updates (compacting, permission mode changes)
SDKCompactBoundaryMessageMarks conversation compaction boundary
SDKRateLimitEventRate limit info: status ("allowed" / "allowed_warning" / "rejected"), resetsAt, utilization
SDKPromptSuggestionMessagePrompt suggestions (requires promptSuggestions: true)
SDKFilesPersistedEventFiles persisted to disk with files[] and failed[]
SDKHookStartedMessage / SDKHookProgressMessage / SDKHookResponseMessageHook execution lifecycle
SDKToolProgressMessage / SDKToolUseSummaryMessageTool execution progress and summaries
SDKTaskStartedMessage / SDKTaskProgressMessage / SDKTaskNotificationMessageBackground task lifecycle
SDKAuthStatusMessageAuthentication status updates

Hooks

Hooks are callback functions that intercept agent events. Use them to block/allow tools, log operations, inject context, or notify external systems.

Hook Events

EventTriggerPythonTypeScript
PreToolUseBefore tool call (can block/modify)
PostToolUseAfter tool result
PostToolUseFailureOn tool error
UserPromptSubmitOn user prompt
StopAgent stops
SubagentStartSubagent spawned
SubagentStopSubagent finishes
NotificationAgent status message
PreCompactBefore conversation compaction
PermissionRequestPermission dialog
SessionStart / SessionEndSession lifecycle
Setup, TeammateIdle, TaskCompletedAdvanced lifecycle
WorktreeCreate / WorktreeRemoveGit worktree events
ConfigChangeConfig file changes

Hook Configuration

# Python
from claude_agent_sdk import query, ClaudeAgentOptions, HookMatcher

async def my_hook(input_data, tool_use_id, context):
    # input_data has: hook_event_name, tool_name, tool_input, session_id, cwd
    file_path = input_data.get("tool_input", {}).get("file_path", "")
    if ".env" in file_path:
        return {
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "permissionDecision": "deny",
                "permissionDecisionReason": "Cannot modify .env files",
            }
        }
    return {}  # Empty = allow

options = ClaudeAgentOptions(
    hooks={
        "PreToolUse": [
            HookMatcher(matcher="Write|Edit", hooks=[my_hook])
        ]
    }
)
// TypeScript
import { query, HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";

const myHook: HookCallback = async (input, toolUseID, { signal }) => {
  const preInput = input as PreToolUseHookInput;
  const toolInput = preInput.tool_input as Record<string, unknown>;
  const filePath = toolInput?.file_path as string;

  if (filePath?.includes(".env")) {
    return {
      hookSpecificOutput: {
        hookEventName: preInput.hook_event_name,
        permissionDecision: "deny",
        permissionDecisionReason: "Cannot modify .env files"
      }
    };
  }
  return {}; // Empty = allow
};

// Usage
const options = {
  hooks: {
    PreToolUse: [{ matcher: "Write|Edit", hooks: [myHook] }]
  }
};

Hook Output Fields

Top-level fields (affect conversation):

  • systemMessage: string — inject context Claude can see
  • continue (continue_ in Python): boolean — whether agent keeps running

hookSpecificOutput fields (affect the current operation):

  • permissionDecision: "allow" | "deny" | "ask"
  • permissionDecisionReason: string — shown to Claude on deny
  • updatedInput: object — modified tool input (requires permissionDecision: "allow")
  • additionalContext: string — append to tool result (PostToolUse only)

Async output (fire-and-forget, for logging/webhooks):

return {"async_": True, "asyncTimeout": 30000}   # Python
return { async: true, asyncTimeout: 30000 };      // TypeScript

For detailed examples (audit logging, sandbox redirection, Slack notifications): see references/hooks.md


Sessions

Sessions maintain full context (files read, analysis, conversation) across multiple queries.

# Python — capture and resume session
session_id = None

# First query
async for message in query(prompt="Read the auth module", options=ClaudeAgentOptions(allowed_tools=["Read", "Glob"])):
    if hasattr(message, "subtype") and message.subtype == "init":
        session_id = message.data.get("session_id")

# Resume with full context
async for message in query(
    prompt="Now find all callers of it",  # "it" = auth module from previous query
    options=ClaudeAgentOptions(resume=session_id),
):
    if hasattr(message, "result"):
        print(message.result)
// TypeScript
let sessionId: string | undefined;

// First query
for await (const message of query({ prompt: "Read the auth module", options: { allowedTools: ["Read", "Glob"] } })) {
  if (message.type === "system" && message.subtype === "init") {
    sessionId = message.session_id;
  }
}

// Resume
for await (const message of query({ prompt: "Now find all callers", options: { resume: sessionId } })) {
  if ("result" in message) console.log(message.result);
}

Fork a Session

Create a new branch from a session without modifying the original:

# Python
options = ClaudeAgentOptions(resume=session_id, fork_session=True)
// TypeScript
const options = { resume: sessionId, forkSession: true };
forkSession: false (default)forkSession: true
Session IDSame as originalNew ID generated
OriginalModifiedPreserved
Use caseContinue conversationExplore alternatives

For multi-turn patterns and session persistence details: see references/sessions.md


Subagents

Subagents are specialized agent instances the main agent can delegate to. Benefits: isolated context, parallel execution, specialized tools/prompts.

Key requirement: Include "Task" in allowedTools — subagents are invoked via the Task tool.

# Python
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition

async for message in query(
    prompt="Use the code-reviewer agent to review auth.py",
    options=ClaudeAgentOptions(
        allowed_tools=["Read", "Glob", "Grep", "Task"],
        agents={
            "code-reviewer": AgentDefinition(
                description="Expert code reviewer. Use for quality, security, and maintainability reviews.",
                prompt="You are a security-focused code reviewer. Identify vulnerabilities and suggest fixes.",
                tools=["Read", "Glob", "Grep"],  # Read-only
                model="sonnet",  # Override model for this subagent
            )
        },
    ),
):
    if hasattr(message, "result"):
        print(message.result)
// TypeScript
for await (const message of query({
  prompt: "Use the code-reviewer agent to review auth.py",
  options: {
    allowedTools: ["Read", "Glob", "Grep", "Task"],
    agents: {
      "code-reviewer": {
        description: "Expert code reviewer. Use for quality, security, and maintainability reviews.",
        prompt: "You are a security-focused code reviewer. Identify vulnerabilities and suggest fixes.",
        tools: ["Read", "Glob", "Grep"],
        model: "sonnet"
      }
    }
  }
})) {
  if ("result" in message) console.log(message.result);
}

AgentDefinition Fields

FieldRequiredPython / TSDescription
descriptionBothWhen to use this agent (determines auto-invocation)
promptBothSubagent's system prompt / expertise
toolsBothAllowed tools (omit to inherit all)
modelBoth"sonnet", "opus", "haiku", "inherit", or model ID
disallowed_tools / disallowedToolsTSBlacklist tools for this subagent
mcp_servers / mcpServersTSSubagent-specific MCP servers
skillsTSSkills this subagent can access
max_turns / maxTurnsTSTurn limit for this subagent

Task Tool Extended Input (TS)

When invoking subagents via the Task tool, TypeScript supports additional parameters:

// Task tool input supports these fields:
{
  description: "Short task description",
  prompt: "Detailed task instructions",
  subagent_type: "code-reviewer",    // Agent name
  model: "sonnet",                    // Override model
  run_in_background: true,            // Run as background task
  resume: "agent-session-id",         // Resume a previous subagent
  max_turns: 10,                      // Limit turns
  isolation: "worktree",              // Run in isolated git worktree
  name: "review-task-1",             // Task identifier
  team_name: "reviewers",            // Team grouping
}

Notes:

  • Subagents cannot spawn their own subagents (don't add "Task" to subagent tools)
  • Use explicit naming in prompt to guarantee a specific subagent: "Use the X agent to..."
  • Messages from subagent context have parent_tool_use_id set

For subagent resumption and parallel patterns: see references/subagents.md


MCP Servers

Connect your agent to databases, browsers, APIs, and hundreds of external tools via the Model Context Protocol.

MCP Tool Naming

MCP tools follow: mcp__<server-name>__<tool-name> Example: server "github" + tool list_issuesmcp__github__list_issues

Add MCP Servers

# Python — connect to GitHub MCP server
from claude_agent_sdk import query, ClaudeAgentOptions
import os

options = ClaudeAgentOptions(
    mcp_servers={
        "github": {
            "command": "npx",
            "args": ["-y", "@modelcontextprotocol/server-github"],
            "env": {"GITHUB_TOKEN": os.environ["GITHUB_TOKEN"]},
        }
    },
    allowed_tools=["mcp__github__list_issues", "mcp__github__search_issues"],
)
// TypeScript
const options = {
  mcpServers: {
    github: {
      command: "npx",
      args: ["-y", "@modelcontextprotocol/server-github"],
      env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN }
    }
  },
  allowedTools: ["mcp__github__list_issues", "mcp__github__search_issues"]
};

Transport Types

TypeWhen to useConfig
stdioLocal CLI tools (npx, uvx)command, args, env
HTTPRemote REST APIstype: "http", url, headers
SSERemote streaming APIstype: "sse", url, headers
SDK MCPIn-process custom toolsSee Custom Tools
claudeai-proxy (TS)Claude.ai proxy serverstype: "claudeai-proxy", url, id
# Remote SSE server with auth
mcp_servers = {
    "remote-api": {
        "type": "sse",
        "url": "https://api.example.com/mcp/sse",
        "headers": {"Authorization": f"Bearer {os.environ['API_TOKEN']}"},
    }
}

Wildcard Tool Access

allowed_tools=["mcp__github__*"]   # All tools from github server

MCP Tool Search (large tool sets)

When tools exceed 10% of context window, auto-activates to load tools on-demand:

env={"ENABLE_TOOL_SEARCH": "auto"}    # default
env={"ENABLE_TOOL_SEARCH": "auto:5"}  # activate at 5% threshold
env={"ENABLE_TOOL_SEARCH": "true"}    # always on
env={"ENABLE_TOOL_SEARCH": "false"}   # always load all upfront

Requires: claude-sonnet-4 or claude-opus-4+. Not available on Haiku.

Dynamic MCP Management (TypeScript)

The TypeScript Query object supports runtime MCP server control:

const q = query({ prompt: "...", options });

// Replace all MCP servers dynamically
const result = await q.setMcpServers({
  "new-server": { command: "npx", args: ["-y", "some-mcp-server"] }
});
// result: { added: ["new-server"], removed: ["old-server"], errors: [] }

// Reconnect a failed server
await q.reconnectMcpServer("github");

// Enable/disable a server
await q.toggleMcpServer("github", false);  // disable
await q.toggleMcpServer("github", true);   // re-enable

For .mcp.json config files and error handling patterns: see references/mcp.md


Custom Tools

Define in-process MCP servers to give Claude custom capabilities without running a separate process.

# Python
from claude_agent_sdk import tool, create_sdk_mcp_server, ClaudeSDKClient, ClaudeAgentOptions
import aiohttp, asyncio
from typing import Any

@tool("get_weather", "Get temperature for coordinates", {"latitude": float, "longitude": float})
async def get_weather(args: dict[str, Any]) -> dict[str, Any]:
    async with aiohttp.ClientSession() as session:
        async with session.get(
            f"https://api.open-meteo.com/v1/forecast?latitude={args['latitude']}&longitude={args['longitude']}&current=temperature_2m"
        ) as response:
            data = await response.json()
    return {"content": [{"type": "text", "text": f"Temp: {data['current']['temperature_2m']}°C"}]}

custom_server = create_sdk_mcp_server(name="weather-tools", version="1.0.0", tools=[get_weather])

async def main():
    options = ClaudeAgentOptions(
        mcp_servers={"weather-tools": custom_server},
        allowed_tools=["mcp__weather-tools__get_weather"],
    )
    async with ClaudeSDKClient(options=options) as client:
        await client.query("What's the weather in Paris (48.85, 2.35)?")
        async for msg in client.receive_response():
            print(msg)

asyncio.run(main())
// TypeScript
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";

const customServer = createSdkMcpServer({
  name: "weather-tools",
  version: "1.0.0",
  tools: [
    tool("get_weather", "Get temperature for coordinates",
      { latitude: z.number(), longitude: z.number() },
      async (args) => {
        const res = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}&current=temperature_2m`);
        const data = await res.json();
        return { content: [{ type: "text", text: `Temp: ${data.current.temperature_2m}°C` }] };
      }
    )
  ]
});

// Important: use async generator for prompt when using SDK MCP servers
async function* messages() {
  yield { type: "user" as const, message: { role: "user" as const, content: "Weather in Paris (48.85, 2.35)?" } };
}

for await (const message of query({
  prompt: messages(),
  options: {
    mcpServers: { "weather-tools": customServer },
    allowedTools: ["mcp__weather-tools__get_weather"]
  }
})) {
  if (message.type === "result") console.log(message.result);
}

Important: Custom MCP tools require streaming/async generator input in TypeScript.

For complex schema patterns, error handling, and database/API gateway examples: see references/custom-tools.md


Skills & Plugins

Skills are reusable capability packages (SKILL.md files) that Claude loads on-demand based on context. The SDK can load skills from the filesystem and invoke them via the Skill built-in tool.

Loading Skills from Filesystem

By default, the SDK does not load any filesystem settings (including skills). You must explicitly configure setting_sources to enable skill discovery.

# Python — enable skills from user and project directories
from claude_agent_sdk import query, ClaudeAgentOptions

options = ClaudeAgentOptions(
    cwd="/path/to/project",                    # Project with .claude/skills/
    setting_sources=["user", "project"],       # Load skills from filesystem
    allowed_tools=["Skill", "Read", "Write", "Bash"],  # Enable Skill tool
)

async for message in query(prompt="Help me process this PDF document", options=options):
    print(message)
// TypeScript — enable skills from user and project directories
for await (const message of query({
  prompt: "Help me process this PDF document",
  options: {
    cwd: "/path/to/project",                   // Project with .claude/skills/
    settingSources: ["user", "project"],        // Load skills from filesystem
    allowedTools: ["Skill", "Read", "Write", "Bash"]  // Enable Skill tool
  }
})) {
  console.log(message);
}

How Skills Work in the SDK

  1. Filesystem artifacts: Skills are SKILL.md files in .claude/skills/ directories
  2. Auto-discovered: Skill metadata (name + description) is loaded at startup
  3. Model-invoked: Claude autonomously decides when to activate a skill based on context
  4. On-demand loading: Full skill content is loaded only when triggered (saves context)

Skill Discovery Locations

setting_sources valueSkills loaded from
"user"~/.claude/skills/
"project".claude/skills/ in cwd
"local".claude/settings.local.json

Discovering Available Skills

# List all skills available to the agent
options = ClaudeAgentOptions(
    setting_sources=["user", "project"],
    allowed_tools=["Skill"],
)

async for message in query(prompt="What Skills are available?", options=options):
    print(message)

Loading Plugins Programmatically

Plugins provide skills, slash commands, and agents from local directories — without requiring them to be in .claude/skills/.

# Python — load plugins from local paths
async for message in query(
    prompt="Hello",
    options={
        "plugins": [
            {"type": "local", "path": "./my-plugin"},
            {"type": "local", "path": "/absolute/path/to/another-plugin"},
        ]
    },
):
    print(message)
// TypeScript — load plugins from local paths
for await (const message of query({
  prompt: "Hello",
  options: {
    plugins: [
      { type: "local", path: "./my-plugin" },
      { type: "local", path: "/absolute/path/to/another-plugin" }
    ]
  }
})) {
  console.log(message);
}

Key differences between Skills and Plugins:

  • Skills must be filesystem artifacts (SKILL.md files) — no programmatic registration API
  • Plugins load from any local path via the plugins array
  • Both require "Skill" in allowed_tools to be invokable
  • Programmatic options (agents, allowed_tools) always override filesystem settings

Permissions & User Input

Permission Modes

ModeBehaviorBest for
"default"No auto-approvals; uses canUseTool callbackInteractive apps
"acceptEdits"Auto-approves file edits + mkdir/rm/mv/cpTrusted dev workflows
"bypassPermissions"All tools run without promptsCI/CD, automation
"plan"No tool execution; Claude plans onlyPre-approval review
"dontAsk"Auto-approve all tools without promptingFully autonomous agents
# Dynamic permission mode change mid-session
q = query(prompt="...", options=ClaudeAgentOptions(permission_mode="default"))
await q.set_permission_mode("acceptEdits")  # Switch after review
async for message in q: ...

canUseTool Callback

Handle interactive tool approvals and AskUserQuestion responses:

# Python
from claude_agent_sdk.types import PermissionResultAllow, PermissionResultDeny

async def can_use_tool(tool_name: str, input_data: dict, context):
    if tool_name == "AskUserQuestion":
        # Claude is asking the user a question
        answers = {}
        for q in input_data.get("questions", []):
            print(f"\n{q['question']}")
            for i, opt in enumerate(q["options"]):
                print(f"  {i+1}. {opt['label']} — {opt['description']}")
            choice = input("Your choice: ")
            answers[q["question"]] = q["options"][int(choice)-1]["label"]
        return PermissionResultAllow(updated_input={"questions": input_data["questions"], "answers": answers})

    # For other tools
    print(f"Claude wants to use {tool_name}: {input_data}")
    response = input("Allow? (y/n): ")
    if response.lower() == "y":
        return PermissionResultAllow(updated_input=input_data)
    return PermissionResultDeny(message="User denied")

# NOTE: Python requires streaming input + dummy PreToolUse hook for canUseTool to work
async def dummy_hook(input_data, tool_use_id, context):
    return {"continue_": True}

async def prompt_stream():
    yield {"type": "user", "message": {"role": "user", "content": "Your task here"}}

options = ClaudeAgentOptions(
    can_use_tool=can_use_tool,
    hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
)
// TypeScript
const options = {
  canUseTool: async (toolName: string, input: any) => {
    if (toolName === "AskUserQuestion") {
      const answers: Record<string, string> = {};
      for (const q of input.questions) {
        console.log(`\n${q.question}`);
        q.options.forEach((opt: any, i: number) => console.log(`  ${i+1}. ${opt.label}`));
        const choice = parseInt(await prompt("Choice: ")) - 1;
        answers[q.question] = q.options[choice].label;
      }
      return { behavior: "allow", updatedInput: { questions: input.questions, answers } };
    }
    console.log(`Tool: ${toolName}`, input);
    const approved = (await prompt("Allow? (y/n): ")).toLowerCase() === "y";
    return approved
      ? { behavior: "allow", updatedInput: input }
      : { behavior: "deny", message: "User denied" };
  }
};

Permission Updates via canUseTool (TypeScript)

The canUseTool callback can return updatedPermissions to dynamically modify permission rules:

const options = {
  canUseTool: async (toolName: string, input: any) => {
    return {
      behavior: "allow",
      updatedInput: input,
      updatedPermissions: {
        // Add new permission rules
        addRules: [{ tool: "Bash(*)", permission: "allow" }],
        // Replace all rules
        replaceRules: [{ tool: "Read(*)", permission: "allow" }],
        // Remove specific rules
        removeRules: [{ tool: "Write(/tmp/*)" }],
        // Change permission mode
        setMode: "acceptEdits",
        // Add/remove accessible directories
        addDirectories: ["/new/path"],
        removeDirectories: ["/old/path"],
      }
    };
  }
};

Permission update destinations: "userSettings", "projectSettings", "localSettings", "session", "cliArg" (TS).

For declarative permission rules (settings.json), plan mode patterns, and input modification: see references/permissions.md


System Prompts & CLAUDE.md

System Prompt Types

The system_prompt option accepts either a plain string or a preset object:

# Plain string — replaces the default system prompt entirely
options = ClaudeAgentOptions(system_prompt="You are a security auditor.")

# Preset — uses Claude Code's full system prompt + your additions
options = ClaudeAgentOptions(
    system_prompt={
        "type": "preset",
        "preset": "claude_code",
        "append": "Always include detailed docstrings and type hints.",
    }
)
// TypeScript preset
const options = {
  systemPrompt: {
    type: "preset",
    preset: "claude_code",
    append: "Always include detailed docstrings and type hints."
  }
};

The preset approach preserves all built-in Claude Code functionality (tool usage, safety, formatting) while adding domain-specific instructions. Use append to add requirements without losing defaults.

CLAUDE.md Files

CLAUDE.md files provide persistent project-level instructions. The SDK loads them only when setting_sources includes "project":

# Required: setting_sources=["project"] to load CLAUDE.md
options = ClaudeAgentOptions(
    system_prompt={"type": "preset", "preset": "claude_code"},
    setting_sources=["project"],  # Loads CLAUDE.md from cwd
)

CLAUDE.md is a markdown file in the project root with guidelines, code style, commands, etc. — Claude follows these instructions automatically.

Output Styles

Output styles are reusable prompt profiles stored in .claude/output-styles/:

# Output styles are auto-loaded when setting_sources includes "user" or "project"
options = ClaudeAgentOptions(
    setting_sources=["user", "project"],  # Loads output styles
)

Create a style file at ~/.claude/output-styles/code-reviewer.md:

---
name: Code Reviewer
description: Thorough code review assistant
---

You are an expert code reviewer. For every submission:
1. Check for bugs and security issues
2. Evaluate performance
3. Suggest improvements

Settings Precedence

When multiple sources are loaded, settings merge (highest to lowest priority):

  1. Programmatic options (agents, allowed_tools) — always win
  2. Local settings (.claude/settings.local.json)
  3. Project settings (.claude/settings.json)
  4. User settings (~/.claude/settings.json)

Cost Tracking

The SDK provides per-message and cumulative cost/usage information.

Usage Fields

FieldTypeDescription
input_tokensnumberBase input tokens processed
output_tokensnumberTokens generated in response
cache_creation_input_tokensnumberTokens used to create cache
cache_read_input_tokensnumberTokens read from cache
total_cost_usdfloatTotal cost (only on ResultMessage)

Tracking Costs

# Python — cost tracking
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ResultMessage

async for message in query(prompt="Refactor auth.py", options=ClaudeAgentOptions()):
    if isinstance(message, AssistantMessage) and hasattr(message, "usage"):
        print(f"Step tokens: in={message.usage.get('input_tokens')}, out={message.usage.get('output_tokens')}")
    elif isinstance(message, ResultMessage):
        print(f"Total: {message.num_turns} turns, {message.duration_ms}ms")
        if message.total_cost_usd:
            print(f"Cost: ${message.total_cost_usd:.4f}")
// TypeScript — cost tracking
for await (const message of query({ prompt: "Refactor auth.py" })) {
  if (message.type === "assistant" && message.message.usage) {
    console.log("Step tokens:", message.message.usage);
  }
  if (message.type === "result") {
    console.log(`Total: ${message.num_turns} turns, ${message.duration_ms}ms`);
    console.log(`Cost: $${message.usage?.total_cost_usd}`);
  }
}

Subagent Costs

The Task tool output includes usage stats for each subagent:

# Task tool output
{
    "result": str,
    "usage": {"input_tokens": int, "output_tokens": int, ...},
    "total_cost_usd": float,
    "duration_ms": int,
}

Structured Outputs

Force Claude to return structured JSON matching a schema. The result appears in ResultMessage.structured_output.

# Python — JSON Schema
options = ClaudeAgentOptions(
    output_format={
        "type": "json_schema",
        "schema": {
            "type": "object",
            "properties": {
                "summary": {"type": "string"},
                "issues": {"type": "array", "items": {"type": "object", "properties": {
                    "severity": {"type": "string", "enum": ["low", "medium", "high", "critical"]},
                    "description": {"type": "string"},
                    "file": {"type": "string"},
                    "line": {"type": "integer"},
                }, "required": ["severity", "description"]}},
            },
            "required": ["summary", "issues"],
        }
    },
    allowed_tools=["Read", "Glob", "Grep"],
)

async for message in query(prompt="Analyze auth.py for security issues", options=options):
    if hasattr(message, "structured_output") and message.structured_output:
        import json
        result = json.loads(message.structured_output)
        for issue in result["issues"]:
            print(f"[{issue['severity']}] {issue['description']}")
// TypeScript — Zod schema (also supports raw JSON Schema)
import { z } from "zod";

const options = {
  outputFormat: {
    type: "json_schema" as const,
    schema: z.object({
      summary: z.string(),
      issues: z.array(z.object({
        severity: z.enum(["low", "medium", "high", "critical"]),
        description: z.string(),
        file: z.string().optional(),
        line: z.number().optional(),
      })),
    })
  },
  allowedTools: ["Read", "Glob", "Grep"]
};

for await (const msg of query({ prompt: "Analyze auth.py", options })) {
  if (msg.type === "result" && msg.structured_output) {
    const result = JSON.parse(msg.structured_output);
    console.log(result.issues);
  }
}

If Claude can't match the schema after retries, the result subtype is "error_max_structured_output_retries".


Thinking & Effort

Control Claude's extended thinking behavior. The thinking option replaces the deprecated max_thinking_tokens.

Thinking Config

# Adaptive (default) — Claude decides when to think
options = ClaudeAgentOptions(thinking={"type": "adaptive"})

# Fixed budget — always think with N tokens
options = ClaudeAgentOptions(thinking={"type": "enabled", "budget_tokens": 10000})

# Disabled — no extended thinking
options = ClaudeAgentOptions(thinking={"type": "disabled"})
const options = { thinking: { type: "adaptive" as const } };
const options2 = { thinking: { type: "enabled" as const, budget_tokens: 10000 } };
const options3 = { thinking: { type: "disabled" as const } };

Effort Levels

A simpler alternative to thinking config — controls thinking depth:

options = ClaudeAgentOptions(effort="high")   # "low", "medium", "high", "max"
const options = { effort: "high" as const };  // "low" | "medium" | "high" | "max"

Betas

Enable experimental SDK features:

# 1M token context window (compatible with Opus 4.6, Sonnet 4.5, Sonnet 4)
options = ClaudeAgentOptions(betas=["context-1m-2025-08-07"])

File Checkpointing

File checkpointing tracks file changes and allows rewinding to a previous state — useful for undoing agent modifications.

Setup

import os
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, UserMessage, ResultMessage

options = ClaudeAgentOptions(
    enable_file_checkpointing=True,
    permission_mode="acceptEdits",
    extra_args={"replay-user-messages": None},  # Required for checkpoint UUIDs
    env={**os.environ, "CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING": "1"},
)
const opts = {
  enableFileCheckpointing: true,
  permissionMode: "acceptEdits" as const,
  extraArgs: { "replay-user-messages": null },
  env: { ...process.env, CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING: "1" }
};

Capture Checkpoint and Rewind

checkpoint_id = None
session_id = None

async with ClaudeSDKClient(options) as client:
    await client.query("Refactor the authentication module")
    async for message in client.receive_response():
        # Capture first user message UUID as restore point
        if isinstance(message, UserMessage) and message.uuid and not checkpoint_id:
            checkpoint_id = message.uuid
        if isinstance(message, ResultMessage):
            session_id = message.session_id

# Later — rewind all file changes
if checkpoint_id and session_id:
    async with ClaudeSDKClient(
        ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
    ) as client:
        await client.query("")  # Empty prompt opens connection
        async for message in client.receive_response():
            await client.rewind_files(checkpoint_id)
            break
// TypeScript — rewind
const rewindQuery = query({
  prompt: "",
  options: { ...opts, resume: sessionId }
});
for await (const msg of rewindQuery) {
  await rewindQuery.rewindFiles(checkpointId);
  break;
}

Interrupt & Stop Reasons

Interrupting a Running Task

Use client.interrupt() to stop a long-running task mid-execution:

from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
import asyncio

async def main():
    options = ClaudeAgentOptions(allowed_tools=["Bash"], permission_mode="acceptEdits")

    async with ClaudeSDKClient(options=options) as client:
        await client.query("Count from 1 to 100 slowly")
        await asyncio.sleep(2)

        await client.interrupt()  # Stop current task
        print("Task interrupted!")

        await client.query("Just say hello instead")  # Send new command
        async for message in client.receive_response():
            pass

asyncio.run(main())

Stop Reasons

The ResultMessage includes a stop_reason field indicating why the agent stopped:

subtypestop_reasonMeaning
"success""end_turn"Agent completed normally
"error_during_execution"variesAn error occurred
"error_max_turns""end_turn" or "tool_use"Hit max_turns limit
"error_max_budget_usd"variesHit max_budget_usd spend cap
"error_max_structured_output_retries"variesFailed to match output_format schema
from claude_agent_sdk import ResultMessage

async for message in query(prompt="...", options=ClaudeAgentOptions(max_turns=3)):
    if isinstance(message, ResultMessage):
        if message.subtype == "error_max_turns":
            print(f"Hit turn limit. Last stop reason: {message.stop_reason}")

ClaudeSDKClient (Multi-Turn Client)

ClaudeSDKClient provides a persistent connection for multi-turn conversations, dynamic control, and advanced operations like interrupt and rewind.

Constructor

# Python
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions

client = ClaudeSDKClient(
    options=ClaudeAgentOptions(...),
    transport=None,  # Optional custom Transport for IPC
)
// TypeScript
import { ClaudeSDKClient } from "@anthropic-ai/claude-agent-sdk";

const client = new ClaudeSDKClient({
  options: { ... },
  transport: undefined  // Optional custom Transport
});

The optional transport parameter allows a custom IPC transport (e.g., for embedding the SDK in a larger system). When omitted, the default process-based transport is used.

Method Reference

MethodDescription
connect(prompt?)Initialize connection (optionally with first prompt)
query(prompt, session_id?)Send a prompt (starts or resumes a session)
receive_messages()Async iterator of all messages from current query
receive_response()Async iterator (alias for receive_messages)
interrupt()Cancel the current running task
set_permission_mode(mode)Change permission mode dynamically
set_model(model)Change model dynamically ("sonnet", "opus", "haiku", or None for default)
get_mcp_status()Get status of all connected MCP servers
get_server_info()Get agent server metadata
rewind_files(user_message_id)Undo file changes to a checkpoint (requires enable_file_checkpointing)
disconnect()Close the connection and clean up

Usage with Context Manager

# Python — recommended: use as async context manager
async with ClaudeSDKClient(options=options) as client:
    await client.query("Analyze this codebase")
    async for message in client.receive_response():
        print(message)

    # Dynamic control mid-session
    await client.set_model("haiku")       # Switch to faster model
    await client.set_permission_mode("acceptEdits")  # Relax permissions

    await client.query("Now fix the top 3 issues")
    async for message in client.receive_response():
        print(message)

    # Inspect MCP server health
    status = await client.get_mcp_status()
    print(status)  # {"server_name": {"status": "connected", ...}, ...}

    # Get server info
    info = await client.get_server_info()
    print(info)  # {"version": "...", ...}
// TypeScript
const client = new ClaudeSDKClient({ options });
await client.connect("Analyze this codebase");

for await (const msg of client.receiveMessages()) {
  console.log(msg);
}

// Dynamic control
await client.setModel("haiku");
await client.setPermissionMode("acceptEdits");

await client.query("Now fix the top 3 issues");
for await (const msg of client.receiveMessages()) {
  console.log(msg);
}

await client.disconnect();

Dynamic Permission Mode on Query

You can also change the permission mode on the query() async generator directly:

q = query(prompt="...", options=ClaudeAgentOptions(permission_mode="default"))
await q.set_permission_mode("acceptEdits")  # Change before iterating
async for message in q:
    print(message)
const q = query({ prompt: "...", options: { permissionMode: "default" } });
await q.setPermissionMode("acceptEdits");
for await (const msg of q) {
  console.log(msg);
}

Sandbox & Secure Deployment

Sandbox Settings

Run the agent in a sandboxed environment with restricted capabilities:

from claude_agent_sdk import query, ClaudeAgentOptions

options = ClaudeAgentOptions(
    sandbox={
        "enabled": True,
        "autoAllowBashIfSandboxed": True,
        "network": {
            "allowLocalBinding": True,
            "allowedDomains": ["api.example.com"],
            "allowManagedDomainsOnly": False,
        },
        "filesystem": {
            "allowWrite": ["/workspace"],
            "denyWrite": ["/etc", "/usr"],
            "denyRead": ["/secrets"],
        },
    }
)

async for message in query(prompt="Build and test my project", options=options):
    print(message)
const options = {
  sandbox: {
    enabled: true,
    autoAllowBashIfSandboxed: true,
    excludedCommands: ["rm -rf /"],           // Block specific commands
    network: {
      allowLocalBinding: true,
      allowedDomains: ["api.example.com"],
      allowUnixSockets: false,
    },
    filesystem: {
      allowWrite: ["/workspace"],
      denyWrite: ["/etc", "/usr"],
      denyRead: ["/secrets"],
    },
  }
};

Sandbox settings:

FieldDescription
enabledEnable sandbox
autoAllowBashIfSandboxedAuto-approve Bash when sandboxed
excludedCommandsBlock specific bash commands
network.allowLocalBindingAllow binding local ports
network.allowedDomainsWhitelist of allowed domains
network.allowManagedDomainsOnlyOnly allow managed domains
network.allowUnixSockets / allowAllUnixSocketsUnix socket access
filesystem.allowWriteWritable paths
filesystem.denyWriteRead-only paths
filesystem.denyReadInaccessible paths

Docker Security Hardening

For production deployments, run agents in hardened containers:

docker run \
  --cap-drop ALL \
  --security-opt no-new-privileges \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=100m \
  --tmpfs /home/agent:rw,noexec,nosuid,size=500m \
  --network none \
  --memory 2g --cpus 2 --pids-limit 100 \
  --user 1000:1000 \
  -v /path/to/code:/workspace:ro \
  agent-image

Key security measures:

  • --cap-drop ALL — remove all Linux capabilities
  • --read-only — immutable root filesystem
  • --network none — no network access (use Unix socket for API)
  • --user 1000:1000 — non-root execution
  • Resource limits (--memory, --cpus, --pids-limit)

SDK Internal Architecture

The SDK never calls the Anthropic API directly. It spawns the claude CLI as a child subprocess and communicates via stdin/stdout using newline-delimited JSON (NDJSON):

┌─────────────────────┐    stdin (NDJSON)          ┌──────────────────┐
│   Your Application  │ ──────────────────────────> │                  │
│   (Python / TS)     │                             │  claude CLI      │
│                     │ <────────────────────────── │  (Node.js)       │
│   SDK (Transport)   │    stdout (NDJSON)          │                  │
│                     │                             │  Calls Anthropic │
│                     │    stderr (debug/logs)      │  API internally  │
└─────────────────────┘ <────────────────────────── └──────────────────┘

Process Spawning

Python — resolves the claude binary in order: bundled CLI (_bundled/), shutil.which("claude"), common locations (~/.npm-global/bin/, /usr/local/bin/, etc.). Spawns via anyio.open_process().

TypeScript — resolves bundled cli.js (in the npm package) or pathToClaudeCodeExecutable. If the path is a JS file, prepends node/bun as runtime. Spawns via child_process.spawn().

Both SDKs always pass these base CLI flags:

["--output-format", "stream-json", "--verbose", "--input-format", "stream-json"]

Environment Inheritance

Both SDKs inherit all environment variables from the parent process and add SDK-specific ones:

# Python (subprocess_cli.py)
process_env = {
    **os.environ,                           # Inherit ALL system env vars
    **self._options.env,                    # User-provided overrides
    "CLAUDE_CODE_ENTRYPOINT": "sdk-py",
    "CLAUDE_AGENT_SDK_VERSION": __version__,
}
// TypeScript
let env = { ...process.env };              // Inherit ALL system env vars
// User env vars spread via options.env
env.CLAUDE_CODE_ENTRYPOINT = "sdk-ts";
env.CLAUDE_AGENT_SDK_VERSION = "0.2.66";

This means the claude child process receives every auth-related variable already set in your shell (ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN, AWS/GCP/Azure credentials, etc.).

IPC Protocol (stream-json)

The SDK and CLI communicate using bidirectional NDJSON (one JSON object per line):

Message types (CLI → SDK via stdout):

TypePurpose
{"type": "system", "subtype": "init"}Session initialization (returns session_id)
{"type": "assistant", "message": {...}}Claude's responses
{"type": "user", "message": {...}}Tool results
{"type": "result", "subtype": "success"}Final result with cost/usage
{"type": "control_request", ...}Permission/hook/MCP requests from CLI

Control messages (SDK → CLI via stdin):

TypePurpose
{"type": "user", "message": {...}}User prompts
{"type": "control_request", "request": {"subtype": "initialize"}}Handshake (register hooks, agents)
{"type": "control_request", "request": {"subtype": "interrupt"}}Interrupt processing
{"type": "control_response", ...}Responses to permission/hook/MCP requests

Initialize handshake (first message after spawn):

{"type": "control_request", "request_id": "req_1", "request": {
  "subtype": "initialize",
  "hooks": {"PreToolUse": [{"matcher": "Edit", "hookCallbackIds": ["h0"]}]},
  "agents": {"reviewer": {"prompt": "Review code", "tools": ["Read"]}}
}}

Permission request (CLI asks SDK to approve a tool):

{"type": "control_request", "request_id": "req_42", "request": {
  "subtype": "can_use_tool", "tool_name": "Bash",
  "input": {"command": "rm -rf /tmp/test"}
}}

Permission response (SDK approves/denies):

{"type": "control_response", "response": {
  "request_id": "req_42", "subtype": "success",
  "response": {"behavior": "allow"}
}}

Other control request subtypes (SDK → CLI): set_permission_mode, set_model, rewind_files, mcp_status, mcp_reconnect, mcp_toggle, mcp_set_servers (TS), stop_task


Authentication & Hosting

API Providers

# Anthropic (default)
export ANTHROPIC_API_KEY=your-key

# Amazon Bedrock
export CLAUDE_CODE_USE_BEDROCK=1
# Configure AWS credentials (aws configure or env vars)

# Google Vertex AI
export CLAUDE_CODE_USE_VERTEX=1
# Configure Google Cloud credentials

# Microsoft Azure AI Foundry
export CLAUDE_CODE_USE_FOUNDRY=1
# Configure Azure credentials

Authentication Resolution Order

The claude child process resolves authentication in this priority:

PriorityVariableBilling
1ANTHROPIC_API_KEYPay-per-use (API credits)
2CLAUDE_CODE_OAUTH_TOKENSubscription quota (Pro/Max/Team)
3CLAUDE_CODE_USE_BEDROCK=1AWS Bedrock pricing
4CLAUDE_CODE_USE_VERTEX=1Google Vertex pricing
5CLAUDE_CODE_USE_FOUNDRY=1Azure AI Foundry pricing

SDK vs CLI Authentication (Official)

Anthropic's official position: the SDK is designed for API key authentication. OAuth/subscription login is not officially supported for the Agent SDK.

Interactive CLI (claude)Agent SDK (official)
API key
OAuth / subscription login❌ Not officially supported
Bedrock / Vertex / Foundry

Using Pro/Max Subscription with the SDK

Since the SDK spawns the claude CLI as a child process and inherits all environment variables (see SDK Internal Architecture), if CLAUDE_CODE_OAUTH_TOKEN is set in your shell and ANTHROPIC_API_KEY is not set, the child process will use your subscription quota.

Warning: This is not officially supported by Anthropic. It works due to the SDK's process-spawning architecture but may break in future updates. Anthropic's ToS prohibit using OAuth tokens in third-party products. Use for personal/local development only.

Step 1: Obtain your OAuth token

# Login to Claude (if not already authenticated)
claude login

# Or explicitly set up the token
claude setup-token
# Opens browser → authenticate → token is stored

Step 2: Ensure no API key overrides it

# IMPORTANT: ANTHROPIC_API_KEY takes priority over OAuth token
# Make sure it's NOT set, or the SDK will use API billing instead
unset ANTHROPIC_API_KEY

# Verify OAuth token is available
echo $CLAUDE_CODE_OAUTH_TOKEN

Step 3: Use the SDK normally

import os
from claude_agent_sdk import query, ClaudeAgentOptions

# Option A: OAuth token already in environment (from `claude login`)
# The SDK inherits it automatically — nothing special needed
async for msg in query(prompt="Hello", options=ClaudeAgentOptions()):
    print(msg)
# Option B: Explicitly pass the OAuth token (useful when ANTHROPIC_API_KEY is also set)
options = ClaudeAgentOptions(
    env={
        "CLAUDE_CODE_OAUTH_TOKEN": os.environ["CLAUDE_CODE_OAUTH_TOKEN"],
        # Omit ANTHROPIC_API_KEY to force OAuth usage
    },
)
async for msg in query(prompt="Hello", options=options):
    print(msg)
# Option C: Remove API key at runtime to force OAuth
import os
os.environ.pop("ANTHROPIC_API_KEY", None)  # Remove if present

async for msg in query(prompt="Hello", options=ClaudeAgentOptions()):
    print(msg)
// TypeScript — same principle
import { query } from "@anthropic-ai/claude-agent-sdk";

// Ensure ANTHROPIC_API_KEY is not set; CLAUDE_CODE_OAUTH_TOKEN will be inherited
delete process.env.ANTHROPIC_API_KEY;

for await (const msg of query({ prompt: "Hello" })) {
  console.log(msg);
}

Why it works: The SDK's SubprocessCLITransport (Python) / ProcessTransport (TypeScript) builds the child environment as {...os.environ, ...options.env}. The claude process receives CLAUDE_CODE_OAUTH_TOKEN and, with no ANTHROPIC_API_KEY present, authenticates via your subscription.

Recommended Authentication for Production

Use CaseRecommended Auth
Personal/local developmentAPI key or OAuth token (see above)
CI/CD pipelinesANTHROPIC_API_KEY or Bedrock/Vertex
Commercial productsANTHROPIC_API_KEY (pay-per-use)
Enterprise / cloud-nativeBedrock, Vertex, or Foundry

Dynamic API key rotation (for vaults/secret managers):

# In .claude/settings.json or via setting_sources
# "apiKeyHelper": "my-script-that-returns-api-key"
# CLAUDE_CODE_API_KEY_HELPER_TTL_MS controls refresh interval

Docker Deployment

FROM node:18-slim
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
ENV ANTHROPIC_API_KEY=""
CMD ["node", "agent.js"]

Filesystem Configuration

Load Claude Code settings (CLAUDE.md, slash commands, skills, agents from .claude/agents/):

options = ClaudeAgentOptions(setting_sources=["project"])

Available setting sources: "project" (.claude/), "user" (~/.claude/), "mcpServers"


Session Discovery

TypeScript provides functions to list past sessions and read their transcripts:

import { listSessions, getSessionMessages } from "@anthropic-ai/claude-agent-sdk";

// List past sessions with metadata
const sessions = await listSessions({ dir: "/path/to/project", limit: 10 });
for (const session of sessions) {
  console.log(`${session.sessionId}: ${session.summary}`);
  console.log(`  Branch: ${session.gitBranch}, Last modified: ${session.lastModified}`);
}

// Read session transcript with pagination
const messages = await getSessionMessages(sessions[0].sessionId, {
  limit: 50,
  offset: 0,
});
for (const msg of messages) {
  console.log(msg);
}

SDKSessionInfo fields: sessionId, summary, lastModified, fileSize, customTitle?, firstPrompt?, gitBranch?, cwd?


TypeScript V2 Preview

The V2 API provides a simpler session-based interface (unstable — may change):

import {
  unstable_v2_createSession,
  unstable_v2_resumeSession,
  unstable_v2_prompt,
} from "@anthropic-ai/claude-agent-sdk";

// Create a session
const session = await unstable_v2_createSession({
  allowedTools: ["Read", "Glob"],
  permissionMode: "bypassPermissions",
});

// Send messages and stream responses
session.send("What files are in this directory?");
for await (const msg of session.stream()) {
  if (msg.type === "result") console.log(msg.result);
}

// Resume later
const resumed = await unstable_v2_resumeSession(session.sessionId, {
  allowedTools: ["Read", "Glob"],
});
resumed.send("Now count the TypeScript files");
for await (const msg of resumed.stream()) {
  if (msg.type === "result") console.log(msg.result);
}

// Close (also supports `await using` for auto-cleanup)
await session.close();

// One-shot convenience function (returns ResultMessage directly)
const result = await unstable_v2_prompt("Hello!", {
  allowedTools: ["Read"],
  permissionMode: "bypassPermissions",
});
console.log(result.result);

Query Object Methods (TS)

The TypeScript Query object (returned by query()) exposes additional methods beyond streaming:

MethodReturnsDescription
interrupt()voidCancel the current running task
rewindFiles(userMessageId, opts?)RewindFilesResultRestore files to checkpoint (canRewind, filesChanged, insertions, deletions)
setPermissionMode(mode)voidChange permission mode dynamically
setModel(model?)voidChange model (null to reset to default)
initializationResult()SDKControlInitializeResponseReturns init data: commands, agents, output_style, available_output_styles, models, account
supportedCommands()SlashCommand[]List of available slash commands (name, description, argumentHint)
supportedModels()ModelInfo[]Available models with displayName, supportsEffort, supportedEffortLevels, supportsAdaptiveThinking
supportedAgents()AgentInfo[]Available agents with name, description, model?
accountInfo()AccountInfoAccount details: email?, organization?, subscriptionType?, apiKeySource?
mcpServerStatus()McpServerStatus[]Status of all MCP servers (name, status, tools, errors)
setMcpServers(servers)McpSetServersResultDynamically replace MCP servers (added, removed, errors)
reconnectMcpServer(name)voidReconnect a failed MCP server
toggleMcpServer(name, enabled)voidEnable/disable an MCP server
stopTask(taskId)voidStop a running background task
streamInput(stream)voidStream input messages for multi-turn conversations
close()voidForcefully terminate and clean up

MCP Server Status values: "connected" | "failed" | "needs-auth" | "pending" | "disabled"


Error Types

The SDK provides typed error classes for error handling:

Python Errors

from claude_agent_sdk import (
    ClaudeSDKError,         # Base error class
    CLIConnectionError,     # Connection to CLI failed
    CLINotFoundError,       # CLI binary not found (has cli_path attribute)
    ProcessError,           # Process failure (has exit_code, stderr attributes)
    CLIJSONDecodeError,     # JSON parse failure (has line, original_error attributes)
)

try:
    async for msg in query(prompt="...", options=options):
        pass
except CLINotFoundError as e:
    print(f"CLI not found at: {e.cli_path}")
except ProcessError as e:
    print(f"Process exited with code {e.exit_code}: {e.stderr}")
except CLIConnectionError as e:
    print(f"Connection failed: {e}")
except CLIJSONDecodeError as e:
    print(f"JSON parse error on line {e.line}: {e.original_error}")
except ClaudeSDKError as e:
    print(f"SDK error: {e}")

TypeScript Errors

import { AbortError } from "@anthropic-ai/claude-agent-sdk";

try {
  for await (const msg of query({ prompt: "...", options })) { /* ... */ }
} catch (e) {
  if (e instanceof AbortError) {
    console.log("Query was aborted");
  }
}

AssistantMessage Error Types

When an AssistantMessage contains an error, the error field has a type: "authentication_failed" | "billing_error" | "rate_limit" | "invalid_request" | "server_error" | "unknown"


Transport & Custom Process Spawning

The SDK abstracts communication with the claude CLI through the Transport layer. By default, it uses a subprocess-based transport (see SDK Internal Architecture for the IPC protocol). You can replace or customize this.

Default Transports

SDKDefault TransportMechanism
PythonSubprocessCLITransportanyio.open_process() → stdin/stdout NDJSON
TypeScriptProcessTransportchild_process.spawn() → stdin/stdout NDJSON

Both read stdout line-by-line: TypeScript uses readline.createInterface(), Python uses anyio.TextReceiveStream with a 1MB JSON buffer.

Custom Transport (Python)

The Transport abstract class enables custom IPC (inter-process communication):

from claude_agent_sdk import Transport

class MyTransport(Transport):
    async def connect(self): ...
    async def write(self, data: str): ...
    async def read_messages(self): ...    # Async iterator
    async def close(self): ...
    def is_ready(self) -> bool: ...
    async def end_input(self): ...

client = ClaudeSDKClient(options=options, transport=MyTransport())

DirectConnectTransport (TypeScript)

Connect to a running claude server via WebSocket:

import { DirectConnectTransport } from "@anthropic-ai/claude-agent-sdk";

const transport = new DirectConnectTransport("ws://localhost:8080");
const q = query({ prompt: "Hello", options, transport });

Custom Process Spawning (TypeScript)

Customize how the claude process is launched (VMs, containers, remote machines) while keeping the SDK's NDJSON protocol handling:

const options = {
  spawnClaudeCodeProcess: (spawnOptions) => {
    // spawnOptions: { command, args, env, cwd, signal }
    const child = spawn("docker", ["run", "--rm", "-i", "agent-image", ...spawnOptions.args], {
      env: spawnOptions.env,
      cwd: spawnOptions.cwd,
      signal: spawnOptions.signal,
    });
    return {
      stdin: child.stdin,
      stdout: child.stdout,
      stderr: child.stderr,
      exitCode: new Promise(resolve => child.on("exit", resolve)),
      kill: () => child.kill(),
    };
  }
};

Common Patterns

Read-Only Analysis Agent

options = ClaudeAgentOptions(
    allowed_tools=["Read", "Glob", "Grep"],
    permission_mode="bypassPermissions",
    system_prompt="You are a code analyst. Never modify files.",
)

CI/CD Automation Agent

options = ClaudeAgentOptions(
    allowed_tools=["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
    permission_mode="bypassPermissions",
    max_turns=20,
)

Multi-Agent Parallel Workflow

# Main agent delegates to specialists
options = ClaudeAgentOptions(
    allowed_tools=["Read", "Glob", "Grep", "Task"],
    agents={
        "security-scanner": AgentDefinition(
            description="Security vulnerability scanner",
            prompt="Find security issues. Report CVEs and OWASP violations.",
            tools=["Read", "Grep", "Glob"],
        ),
        "test-runner": AgentDefinition(
            description="Test execution and analysis specialist",
            prompt="Run tests and analyze failures.",
            tools=["Bash", "Read", "Grep"],
        ),
    },
)

Audit Logging Hook

async def audit_logger(input_data, tool_use_id, context):
    with open("audit.log", "a") as f:
        f.write(f"{datetime.now()}: {input_data['tool_name']} — {input_data.get('tool_input', {})}\n")
    return {}

options = ClaudeAgentOptions(
    hooks={"PostToolUse": [HookMatcher(hooks=[audit_logger])]}
)

Skills-Enabled Agent

# Agent that can use installed skills (PDF, XLSX, etc.)
options = ClaudeAgentOptions(
    cwd="/path/to/project",
    setting_sources=["user", "project"],  # Load skills from filesystem
    allowed_tools=["Skill", "Read", "Write", "Edit", "Bash", "Glob"],
)

Todo Progress Tracking

# Monitor TodoWrite tool calls to track agent progress in real-time
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage

async for message in query(prompt="Refactor the auth module", options=ClaudeAgentOptions(
    allowed_tools=["Read", "Edit", "Write", "Glob", "Grep", "TodoWrite"],
)):
    if isinstance(message, AssistantMessage):
        for block in message.content:
            if hasattr(block, "name") and block.name == "TodoWrite":
                todos = block.input.get("todos", [])
                for todo in todos:
                    status = todo.get("status", "")
                    label = todo.get("content", "")
                    if status == "in_progress":
                        print(f"  ▶ {todo.get('activeForm', label)}")
                    elif status == "completed":
                        print(f"  ✓ {label}")

Session-Based Multi-Turn Workflow

session_id = None
prompts = ["Read auth.py", "Find security issues", "Fix the top 3 issues"]

for prompt_text in prompts:
    opts = ClaudeAgentOptions(
        allowed_tools=["Read", "Edit", "Glob"],
        resume=session_id,
    )
    async for message in query(prompt=prompt_text, options=opts):
        if hasattr(message, "subtype") and message.subtype == "init":
            session_id = message.data.get("session_id")
        if hasattr(message, "result"):
            print(message.result)

Reference Files

For detailed coverage of each topic, read the appropriate reference:

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

claude-agent-sdk

No summary provided by upstream source.

Repository SourceNeeds Review
422-jezweb
Automation

claude-agent-sdk

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

claude-agent-sdk

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

claude-agent-sdk

No summary provided by upstream source.

Repository SourceNeeds Review