Claude Code Agent SDK Python TypeScript AI Agents developer tools

Claude Agent SDK: Build Custom AI Agents with Python and TypeScript (2026)

The Prompt Shelf ·

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.

ToolWhat it doesTypical use
ReadRead any file in the working directoryCodebase analysis, reading configs
WriteCreate new filesGenerating reports, scaffolding
EditMake precise edits to existing filesBug fixes, refactoring
BashRun terminal commandsTests, git, builds, scripts
MonitorWatch a background script; react to each output lineLong-running process monitoring
GlobFind files by pattern (**/*.ts, src/**/*.py)Scanning codebase structure
GrepSearch file contents with regexFinding usages, cross-references
WebSearchSearch the webResearch, finding docs
WebFetchFetch and parse web pagesReading documentation, specs
AskUserQuestionAsk clarifying questions with multiple-choice optionsInteractive 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 SDKClaude Code CLIManaged Agents
InterfacePython/TypeScript libraryInteractive terminalREST API
Runs inYour processYour terminalAnthropic infrastructure
File accessYour filesystemYour filesystemManaged sandbox
Custom toolsHooks + MCPHooks + MCPREST callbacks
Session stateJSONL on diskInteractiveAnthropic-hosted
Best forCI/CD, applications, automationDaily developmentProduction 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.


Related Articles

Explore the collection

Browse all AI coding rules — CLAUDE.md, .cursorrules, AGENTS.md, and more.

Browse Rules