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 | |
|---|---|---|
| Shape | Async function returning an iterator of messages | Bidirectional conversation client |
| Best for | One-shot tasks: give a job → get a result | Multi-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
| Option | Meaning |
|---|---|
system_prompt | The agent’s role and rules |
max_turns | Caps turns — stops expensive loops |
allowed_tools | Tools that are auto-approved — note: not the list of available tools; others still exist |
disallowed_tools | Blocks tools outright (for a read-only agent, block Write/Edit/Bash here) |
permission_mode | e.g. "acceptEdits" to auto-accept file edits |
cwd | Working directory |
mcp_servers | MCP servers — both in-process (SDK) and external |
hooks | Python 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
| Need | Choose |
|---|---|
| Short scripts, cron, simple CI | claude -p |
| Custom tools calling internal systems (Jira, DBs, company APIs) | SDK |
| Structured message handling, progress in your own UI | SDK |
| Hooks/permissions with complex logic | SDK |
| Embedding an agent in a backend/service | SDK |
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
- anthropics/claude-agent-sdk-python — GitHub
- Agent SDK reference — Python — Claude Docs