Claude Agent SDK: Building Agents Programmatically
Everything you do in the terminal by hand — claude reads a file, finds a bug, fixes it, runs tests — can be programmed. The Claude Agent SDK is a TypeScript and Python library that provides the same agentic loop, the same tools, and the same memory model that underlie interactive Claude Code. The only difference is that instead of a terminal session, you write regular code.
Until recently, the library was called "Claude Code SDK" — worth keeping in mind if you come across older articles. The official name is now: Claude Agent SDK.
Important as of today. Starting June 15, 2026, Agent SDK usage on subscription plans (Pro/Max/Team/Enterprise) is billed via a separate monthly Agent SDK Credit, independent of the interactive usage limit. See the Anthropic support article for details.
Installation
# TypeScript (Node.js 18+)
npm install @anthropic-ai/claude-agent-sdk
# Python (3.10+)
pip install claude-agent-sdkThe TypeScript package ships with a native Claude Code binary for your platform — no separate CLI installation required. Python requires 3.10+: if you get No matching distribution found, check python3 --version.
Authentication: set the ANTHROPIC_API_KEY environment variable. The SDK also supports Amazon Bedrock (CLAUDE_CODE_USE_BEDROCK=1), Google Vertex AI (CLAUDE_CODE_USE_VERTEX=1), and Microsoft Azure Foundry (CLAUDE_CODE_USE_FOUNDRY=1) — for enterprise environments where data region control is required.
query(): The Heart of the SDK
The single entry point is the query() function. It starts the agentic loop and returns an async iterator: each iteration yields one event from Claude's stream of work.
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def main():
async for message in query(
prompt="Find and fix the division-by-zero bug in utils.py",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Edit", "Glob"],
permission_mode="acceptEdits",
),
):
if hasattr(message, "result"):
print(message.result)
asyncio.run(main())The same in TypeScript:
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Find and fix the division-by-zero bug in utils.ts",
options: {
allowedTools: ["Read", "Edit", "Glob"],
permissionMode: "acceptEdits"
}
})) {
if ("result" in message) console.log(message.result);
}Note the naming difference: Python uses snake_case (allowed_tools, permission_mode), TypeScript uses camelCase (allowedTools, permissionMode). The concepts are identical.
The async for loop runs while Claude thinks, invokes tools, and observes results — and decides on its own when the task is complete. You simply read the stream.
sequenceDiagram
participant App as Your code
participant SDK as Agent SDK
participant Claude as Claude
participant Tools as Tools (Read/Edit/Bash...)
App->>SDK: query(prompt, options)
SDK->>Claude: prompt + tool list
Claude-->>App: SystemMessage (session_id)
loop Agent loop
Claude->>Claude: reasoning
Claude-->>App: AssistantMessage (text)
Claude->>Tools: tool call
Tools-->>Claude: tool result
Claude-->>App: AssistantMessage (tool call)
end
Claude-->>App: ResultMessage (success/error)
SDK-->>App: iterator closedMessage Types in the Stream
Without filtering, you receive a "raw" stream. Three key types:
| Type | When | Contents |
|---|---|---|
SystemMessage (subtype init) | First | session_id — needed to resume a session |
AssistantMessage | Continuously | content — Claude's reasoning and tool calls |
ResultMessage | Last | result — the outcome; subtype — success or error |
For production code, filter for ResultMessage only; for debugging, print everything:
from claude_agent_sdk import AssistantMessage, ResultMessage, SystemMessage
async for message in query(prompt="...", options=opts):
if isinstance(message, SystemMessage) and message.subtype == "init":
session_id = message.data["session_id"] # save for resume
elif isinstance(message, AssistantMessage):
for block in message.content:
if hasattr(block, "text"):
print("Claude:", block.text) # reasoning
elif hasattr(block, "name"):
print("Tool:", block.name) # tool call
elif isinstance(message, ResultMessage):
print("Done:", message.subtype) # success / errorTools and Permission Modes
allowed_tools is the list of tools that the SDK approves automatically without prompting. All Claude Code tools are available:
| Tool | What it does |
|---|---|
Read, Write, Edit | File operations |
Bash | Terminal commands, git, npm |
Glob, Grep | File and content search |
WebSearch, WebFetch | Web search |
Monitor | Watch a background process |
Agent | Launch subagents (must be explicitly included) |
AskUserQuestion | Ask the user a clarifying question |
Permission modes define what happens with tools not in allowed_tools:
| Mode | Behavior | When to use |
|---|---|---|
acceptEdits | Auto-approves file operations, prompts for everything else | Trusted dev scenarios |
dontAsk | Blocks everything not in allowed_tools | Headless agents with strict control |
auto (TS only) | A classifier model decides autonomously | Autonomous agents with guardrails |
bypassPermissions | Executes everything without prompting | Isolated CI environments, sandboxes |
default | Requires a canUseTool callback | Custom flows with manual approval |
Hooks: Programmatic Callbacks
In settings.json, hooks are shell commands. In the SDK, hooks are functions defined directly in your code. The concept is the same; the implementation is different.
from claude_agent_sdk import query, ClaudeAgentOptions, HookMatcher
from datetime import datetime
async def audit_file_change(input_data, tool_use_id, context):
path = input_data.get("tool_input", {}).get("file_path", "unknown")
with open("audit.log", "a") as f:
f.write(f"{datetime.now()}: modified {path}\n")
return {} # empty dict = allow; {"decision": "block"} = deny
async for message in query(
prompt="Refactor utils.py",
options=ClaudeAgentOptions(
permission_mode="acceptEdits",
hooks={
"PostToolUse": [
HookMatcher(matcher="Edit|Write", hooks=[audit_file_change])
]
},
),
):
...Available events: PreToolUse, PostToolUse, Stop, SessionStart, SessionEnd, UserPromptSubmit. A hook returns {} (continue) or {"decision": "block", "reason": "..."} (block). This gives you full programmatic control over the agent's lifecycle.
Subagents, Sessions, and MCP
Subagents are declared directly in options and launched via the Agent tool (which must be added to allowed_tools):
from claude_agent_sdk import AgentDefinition
options = ClaudeAgentOptions(
allowed_tools=["Read", "Glob", "Grep", "Agent"],
agents={
"code-reviewer": AgentDefinition(
description="Expert code reviewer",
prompt="Analyze code quality and security. Be concise.",
tools=["Read", "Glob", "Grep"],
)
},
)Sessions preserve context across multiple query() calls. Capture the session_id from the first call and pass it as resume:
# The second call continues the first — "it" is understood without repeating context
async for message in query(
prompt="Now find all callers of the function we just analyzed",
options=ClaudeAgentOptions(resume=session_id),
):
...MCP servers are connected via the mcp_servers option using the same structure as .mcp.json:
options = ClaudeAgentOptions(
mcp_servers={
"playwright": {"command": "npx", "args": ["@playwright/mcp@latest"]}
}
)For more on MCP servers as a concept, see Model Context Protocol: Architecture and Fundamentals.
GitHub Action — A Wrapper Around the Agent SDK
If you understand the SDK, the GitHub Action becomes more transparent. anthropics/claude-code-action is officially described as "built on top of the Claude Agent SDK." The Action runs query() inside a GitHub runner, passing the event context (issue body, PR diff, a comment mentioning @claude) as the prompt.
# What the action effectively does under the hood:
for await (const message of query({
prompt: buildPromptFromGitHubEvent(event),
options: {
allowedTools: parseAllowedTools(claude_args),
permissionMode: "acceptEdits"
}
})) { ... }This explains why claude_args in the Action's YAML file accepts the same flags as the CLI: --max-turns, --model, --allowedTools. Under the hood, it's the same engine.
If the built-in Action isn't enough, you can use the SDK directly inside a runs-on: ubuntu-latest job and build any custom logic you need. For more on the Action itself, see GitHub Actions and Automated Code Review.
SDK, CLI, or Client SDK: What to Choose
The decision path is straightforward.
Agent SDK vs CLI: one engine, different interfaces. The CLI is for daily development at the terminal. The SDK is for CI/CD, custom applications, and automation. Many teams use both.
Agent SDK vs Anthropic Client SDK (Claude API and Anthropic SDK: Fundamentals): the key difference is who implements the tool loop:
# Client SDK: you implement the loop yourself
response = client.messages.create(...)
while response.stop_reason == "tool_use":
result = your_tool_executor(response.tool_use)
response = client.messages.create(tool_result=result, ...)
# Agent SDK: Claude manages the loop itself
async for message in query(prompt="Fix the bug"):
print(message)The Client SDK gives you maximum control over every step and cost; the Agent SDK gives you development speed and a ready-made agentic infrastructure.
Agent SDK vs Managed Agents: the SDK runs the agentic loop in your process and on your infrastructure. Managed Agents (a hosted REST API) have Anthropic managing the sandbox and sessions — you only submit tasks. A typical path is: prototype with the SDK → migrate to Managed Agents for production when needed.
What Gets Loaded from the Filesystem
By default, the SDK picks up configuration from the launch directory, just as an interactive Claude Code session does:
| Path | What is loaded |
|---|---|
CLAUDE.md / .claude/CLAUDE.md | Project memory, instructions |
.claude/skills/*/SKILL.md | Skills |
.claude/commands/*.md | Custom commands |
To isolate the agent from the local environment (for example, in CI), use setting_sources=[] — the SDK will not read anything from the filesystem and will rely only on options provided in code.
See also
- Claude API and Anthropic SDK: Fundamentals — the Messages API and Client SDK, which the Agent SDK is not but is often compared to
- Tool Use, MCP, and Streaming in the API — how tool use works at the API level, for when you need full control
- Prompt Caching, Batches, and Cost Optimization — how to reduce the cost of agentic requests
- GitHub Actions and Automated Code Review — claude-code-action as a high-level wrapper around the SDK
- Subagents and Context Isolation — the subagent concept, applicable in the SDK as well
- Hooks — Lifecycle Events — hooks in
settings.json, the interactive-session counterpart to SDK hooks - Model Context Protocol: Architecture and Fundamentals — MCP, connected via
mcp_serversin SDK options - Headless Mode and Scripting via CLI — an alternative automation approach using
claude -p