AI Agents & Skills

Claude Agent SDK (Python): build your own agent on Claude Code's engine

TL;DR

pip install claude-agent-sdk (Python 3.10+, Claude Code CLI bundled). Use query() for one-way tasks and ClaudeSDKClient for two-way conversations, custom tools and hooks. Custom tools run in-process via create_sdk_mcp_server — no separate MCP process.

Claude Code is powerful because it ships a complete agent “engine”: the think–act loop, tools for reading/editing files and running commands, context management, hooks and permissions. The Claude Agent SDK lets you use that same engine from your own code — to build team-specific agents, embed them in a backend, or automate workflows where claude -p isn’t flexible enough. This article focuses on the Python SDK.

Installation

pip install claude-agent-sdk     # Python 3.10+

The package bundles the Claude Code CLI, so there’s nothing else to install. To use a specific CLI version, pass ClaudeAgentOptions(cli_path="/path/to/claude"). Authenticate with ANTHROPIC_API_KEY (or Bedrock / Google Cloud / Foundry credentials). Note: when you build a business on the SDK, use an API key — not subscription tokens.

Two ways to call it

query()ClaudeSDKClient
ShapeAsync function returning an iterator of messagesBidirectional conversation client
Best forOne-shot tasks: give a job → get a resultMulti-turn conversations, custom tools, hooks

Minimal query()

import anyio
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, TextBlock

async def main():
    opts = ClaudeAgentOptions(
        system_prompt="You are a reviewer. Read code only, never edit.",
        allowed_tools=["Read", "Grep", "Glob"],
        cwd="./my-repo",
        max_turns=5,
    )
    async for msg in query(prompt="Find error handling that doesn't log", options=opts):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if isinstance(block, TextBlock):
                    print(block.text)

anyio.run(main)

Key options

OptionMeaning
system_promptThe agent’s role and rules
max_turnsCaps turns — stops expensive loops
allowed_toolsTools that are auto-approved — note: not the list of available tools; others still exist
disallowed_toolsBlocks tools outright (for a read-only agent, block Write/Edit/Bash here)
permission_modee.g. "acceptEdits" to auto-accept file edits
cwdWorking directory
mcp_serversMCP servers — both in-process (SDK) and external
hooksPython functions run at events such as PreToolUse

By default the agent has Claude Code’s full toolset (Read, Write, Edit, Bash…) — so always configure permissions explicitly.

Custom tools in Python — no separate MCP server

This is the SDK’s biggest strength: write tools as Python functions that run in your own process.

from claude_agent_sdk import tool, create_sdk_mcp_server, ClaudeAgentOptions, ClaudeSDKClient

@tool("get_sprint", "Get a team's current sprint", {"team": str})
async def get_sprint(args):
    data = await jira_client.current_sprint(args["team"])   # your code
    return {"content": [{"type": "text", "text": str(data)}]}

server = create_sdk_mcp_server(name="pm", version="1.0.0", tools=[get_sprint])

opts = ClaudeAgentOptions(
    mcp_servers={"pm": server},
    allowed_tools=["mcp__pm__get_sprint"],
)

async with ClaudeSDKClient(options=opts) as client:
    await client.query("How many stories are still open in the Mobile team's sprint?")
    async for msg in client.receive_response():
        print(msg)

Compared with external MCP servers: no subprocess management, no IPC overhead, simpler deployment and debugging, and proper Python types. You can still mix SDK servers and external servers in one mcp_servers dict.

Hooks in code: hard rules for the agent

from claude_agent_sdk import ClaudeAgentOptions, HookMatcher

async def block_prod_db(input_data, tool_use_id, context):
    if input_data["tool_name"] != "Bash":
        return {}
    cmd = input_data["tool_input"].get("command", "")
    if "prod-db" in cmd:
        return {"hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": "Production database access is not allowed",
        }}
    return {}

opts = ClaudeAgentOptions(
    allowed_tools=["Bash"],
    hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[block_prod_db])]},
)

Hooks are invoked by the application by fixed rules — they don’t depend on the model “remembering” anything.

Error handling

from claude_agent_sdk import CLINotFoundError, ProcessError, CLIJSONDecodeError

try:
    async for m in query(prompt="Hello"):
        pass
except CLINotFoundError:
    print("Claude Code CLI not installed")
except ProcessError as e:
    print("Process failed, exit code:", e.exit_code)
except CLIJSONDecodeError as e:
    print("Couldn't parse the response:", e)

Upgrading from the old claude-code-sdk package (before 0.1.0): rename ClaudeCodeOptions to ClaudeAgentOptions and check the CHANGELOG for other changes.

When to use the SDK instead of claude -p

NeedChoose
Short scripts, cron, simple CIclaude -p
Custom tools calling internal systems (Jira, DBs, company APIs)SDK
Structured message handling, progress in your own UISDK
Hooks/permissions with complex logicSDK
Embedding an agent in a backend/serviceSDK

How to apply it

  • Developers: Start with a read-only agent (Read/Grep/Glob) that answers codebase questions for new joiners — about 30 lines of code.
  • Tech Leads: Wrap internal operations (sprint lookups, log searches, ticket creation) as custom tools; agents can use them immediately without a separate MCP server.
  • PMs: The SDK is the foundation for an internal “PM assistant”: reading Jira, compiling progress, drafting reports — read-only, with human approval before anything is sent.

Sources & references

  1. anthropics/claude-agent-sdk-python — GitHub
  2. Agent SDK reference — Python — Claude Docs