Claude Agent SDK Guide: TypeScript & Python Examples
The Claude Agent SDK is Anthropic's library for building autonomous agents in Python and TypeScript. It gives you the same agent loop, built-in tools and context management that run Claude Code, packaged so you can call it from your own code instead of a terminal. Until September 2025 it was called the Claude Code SDK.
That last sentence is why you'll find two names, two package names and a pile of outdated tutorials when you search for it. This guide is the version I wanted on day one: one running example, both languages side by side, and the traps marked before you fall into them.
I build accounts-payable automation for small businesses, so the running example is an invoice agent. It reads invoice PDFs from a folder, checks the supplier against a database through a custom tool, is blocked from touching .env, and hands back typed JSON at the end. TypeScript first, Python alongside, and a short Go section near the end for people like me who'd rather not run Node in production if they can avoid it.
Everything here is checked against the September 2026 docs, TypeScript SDK 0.3.273 and Python SDK 0.2.152. Anthropic ships a new SDK build most days, so if something looks different by the time you read this, the docs links at the bottom win.
What the Claude Agent SDK is
The short version: Claude Code is an agent, and the SDK is that agent with the terminal UI removed. Same loop (gather context, act, verify, repeat), same tools (Read, Bash, Glob and friends), same automatic compaction when the context fills up. You import it, hand it a prompt and some options, and it goes off and does the thing while streaming messages back to you.
That's the whole pitch, and it's why the SDK is interesting compared to writing your own loop around the Messages API. Anthropic has spent a year and a half hardening Claude Code's loop against real codebases and real users. You get that work for free, plus the bugs, which is the deal with every framework.
From Claude Code SDK to Claude Agent SDK
Anthropic renamed the Claude Code SDK to the Claude Agent SDK on 29 September 2025. The reasoning in the announcement post was that the harness powering Claude Code can power other kinds of agents too, so "Code" in the name was underselling it. Fair enough. The rename still left a trail of confusion that shows up in search results and in half the tutorials out there.
| Old | New | |
|---|---|---|
| npm package | @anthropic-ai/claude-code |
@anthropic-ai/claude-agent-sdk |
| PyPI package | claude-code-sdk |
claude-agent-sdk |
| Python import | claude_code_sdk |
claude_agent_sdk |
| Python options type | ClaudeCodeOptions |
ClaudeAgentOptions |
Two traps here. The old npm name @anthropic-ai/claude-code still exists, but it's the Claude Code CLI now, not the SDK. And the old PyPI package is frozen at 0.0.25 and prints a migration notice when you import it.
The rename came with two behaviour changes in v0.1.0 that bite anyone migrating old code.
The system prompt is no longer Claude Code's by default. You get a minimal prompt instead. If you want Claude Code's coding instructions and personality back, ask for the preset: systemPrompt: { type: "preset", preset: "claude_code" } in TypeScript, or system_prompt={"type": "preset", "preset": "claude_code"} in Python. Both take an append field for your own additions on top.
Settings loading went back and forth. v0.1.0 briefly stopped loading filesystem settings, then Anthropic reverted that. Today, if you leave settingSources out, the SDK loads your user, project and local settings (~/.claude/settings.json, .claude/settings.json, .claude/settings.local.json, plus CLAUDE.md and custom commands), the same as the CLI would. Pass settingSources: [] when you want a clean, isolated agent, which is what you want for anything deployed. More on that in the production section. One wrinkle: Python SDK 0.1.59 and earlier treated an empty list the same as leaving the option out, so don't rely on [] on an old install.
Agent SDK vs Client SDK vs Claude Code vs Managed Agents
Anthropic now has four ways to point Claude at a task, and the names overlap enough that I made a table.
| Product | Who runs the agent loop | Where it runs | Use it when |
|---|---|---|---|
| Client SDK (Messages API) | You | Your process | You want full control over orchestration, retries and state. Seven languages, including Go. |
| Claude Code CLI | Claude Code | Your terminal | Interactive development, or one-off scripted tasks with claude -p |
| Claude Agent SDK | Claude Code's loop | Your process (Python or TypeScript) | You want the Claude Code loop inside your own app or service |
| Claude Managed Agents | Anthropic | Anthropic's infrastructure | Long-running or async agents where you'd rather not operate the sandbox yourself |
Managed Agents deserves a paragraph because people mix it up with the SDK constantly. It launched in public beta on 8 April 2026 and it's a hosted REST API: Anthropic runs the loop, the sandbox and the session log, and your app sends events and reads results. Pricing is standard token rates plus $0.08 per active session-hour, and data residency is US-only during the beta. Code written against the Agent SDK doesn't deploy to it directly. They share the concepts, not the API.
OpenAI's lineup has the same shape, which helps if you're comparing. The OpenAI Agents SDK is an open-source framework you host yourself. The OpenAI Agents API, which went into public beta on 10 September 2026, is the managed Codex harness behind one API call, US-only, with no zero-data-retention option in any configuration. So the like-for-like comparison is Claude Agent SDK vs OpenAI Agents SDK, and Claude Managed Agents vs OpenAI Agents API. Anyone comparing the Claude Agent SDK to the OpenAI Agents API is comparing a library to a hosting product.
When not to use it
If you want to decide every step yourself (which tool runs when, how retries work, what gets persisted where), the SDK will fight you. Use the Messages API and write the loop. If you don't want to operate a sandbox at all, Managed Agents exists for that. And if you're weighing alternatives, I spent a week with the Pi coding agent's TypeScript SDK last month. Different trade-offs, worth a look if you're not committed to Claude.
Quickstart: your first agent in TypeScript and Python
Install and authenticate
- You need Node 18 or newer, or Python 3.10 or newer. The Claude Code binary ships inside the package, so there's nothing else to install. Two exceptions:
npm ci --omit=optionalskips the platform binary, and a pip install that falls back to the sdist (ARM64 Windows does this) gives you no binary either. In both cases install Claude Code natively and point the SDK at it withpathToClaudeCodeExecutable(TypeScript) orcli_path(Python). - Install the package.
- Set
ANTHROPIC_API_KEYin your process environment. The SDK does not read.envfiles. Load them yourself withdotenvor whatever you use, before the firstquery()call. It's the first thing to check when you get an auth error and you're certain the key is right. - If you're on a cloud provider instead, it's one env var each:
CLAUDE_CODE_USE_BEDROCK=1,CLAUDE_CODE_USE_VERTEX=1orCLAUDE_CODE_USE_FOUNDRY=1, plus that provider's usual credentials.
# TypeScript
npm init -y && npm pkg set type=module
npm install @anthropic-ai/claude-agent-sdk zod
npm install --save-dev tsx
# Python
python3 -m venv .venv && source .venv/bin/activate
pip install claude-agent-sdkYour first agent in TypeScript
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Read every PDF in ./invoices and list the invoice number, supplier and total for each.",
options: {
allowedTools: ["Read", "Glob"],
permissionMode: "dontAsk",
},
})) {
if (message.type === "assistant") {
for (const block of message.message.content) {
if (block.type === "text") console.log(block.text);
}
} else if (message.type === "result") {
console.log(`Done (${message.subtype}), cost $${message.total_cost_usd?.toFixed(4)}`);
}
}query() returns an async generator. You iterate it, messages arrive as Claude works, and the last one is a result. allowedTools pre-approves Read and Glob. permissionMode: "dontAsk" turns anything that would have prompted a human into a denial. That pairing is the docs' recommended shape for a headless agent, and it's what I use as the starting point for everything.
Run it with npx tsx agent.ts. It reads the PDFs (the Read tool handles PDFs and images natively) and prints a list.
The same agent in Python
import asyncio
from claude_agent_sdk import (
query, ClaudeAgentOptions, AssistantMessage, ResultMessage, TextBlock,
)
async def main():
async for message in query(
prompt="Read every PDF in ./invoices and list the invoice number, supplier and total for each.",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Glob"],
permission_mode="dontAsk",
),
):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(block.text)
elif isinstance(message, ResultMessage):
print(f"Done ({message.subtype}), cost ${message.total_cost_usd or 0:.4f}")
asyncio.run(main())Same shape, snake_case option names, and isinstance checks instead of a type discriminator. Python also has ClaudeSDKClient, a stateful client for multi-turn conversations: you open it with async with, call client.query() as many times as you like, and read replies with client.receive_response(). Reach for it when the conversation continues past one prompt or when you want to change the permission mode mid-session with set_permission_mode(). For a one-shot job like this, query() is enough.
Reading messages, results and cost
There are four message types you'll deal with in practice.
A system message with subtype init arrives first. It carries the session ID and the lists of tools, skills and slash commands that loaded. When something isn't behaving, this is where you check whether it loaded at all.
assistant messages hold content blocks: text and tool_use. Watching the tool_use blocks is how you see what the agent is doing.
user messages carry tool results coming back into the conversation. Each has a uuid, which matters later for file checkpointing.
The result message ends the turn. subtype is success or one of several error variants (error_max_turns, error_max_budget_usd, and so on). It carries the final text in result, plus total_cost_usd, a usage block and a per-model modelUsage breakdown. If you asked for structured output, it's on structured_output.
One thing that surprised me: a one-shot query() throws (TypeScript) or raises (Python) after yielding an error result. So you get the error message in the stream and then an exception. Wrap the loop in a try if you're processing a batch and want to continue past a failure.
Tools, MCP servers and Skills
Built-in tools and the two layers of control
Out of the box the agent has Read, Write, Edit, Bash, Glob, Grep, WebSearch, WebFetch, an Agent tool for delegating to subagents, a Skill tool, and AskUserQuestion. The todo and task-tracking tools (TodoWrite, TaskCreate and friends) used to be there too, but on Sonnet 5, Opus 4.8, Fable 5, Mythos 5 and newer they're off by default. If you want them, name one in allowedTools, which opts the session in, or set CLAUDE_CODE_ENABLE_TODO_TOOLS=1.
The thing that took me longest to internalise is that there are two separate layers of control, and the option names don't make it obvious.
Availability is whether a tool appears in Claude's context at all. tools: ["Read", "Grep"] means only those built-ins exist. tools: [] removes every built-in so Claude can only use your MCP tools. A bare name in disallowedTools, like "Bash", also removes the tool from context.
Permission is whether a call gets approved once Claude attempts it. allowedTools is an auto-approve list. Tools you don't list are still available; a call to one of them just goes through the permission flow (mode, then your callback) instead of running immediately. A scoped deny rule like "Bash(rm *)" leaves Bash in context and blocks only matching calls.
So allowedTools doesn't restrict anything. It approves. If you want a tool gone, remove it from tools or deny it by bare name. Otherwise Claude may burn a turn trying it and getting refused.
Custom tools with in-process MCP servers
This is where the invoice agent starts earning its keep. We want Claude to check the supplier on each invoice against our own records, so we give it a lookup_supplier tool. In the SDK, a custom tool is an MCP tool running inside your process. No subprocess, no network hop, no separate server to deploy.
In TypeScript you define the input schema with Zod (3 or 4 both work) and the handler's args are typed from it:
import { tool, createSdkMcpServer, query } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
const lookupSupplier = tool(
"lookup_supplier",
"Look up a supplier by ABN. Returns the registered name, payment terms and whether we've paid them before.",
{ abn: z.string().length(11).describe("Australian Business Number, digits only") },
async ({ abn }) => {
const supplier = await db.suppliers.findByAbn(abn);
if (!supplier) {
return {
content: [
{
type: "text",
text: `No supplier on file with ABN ${abn}. Treat this invoice as new-supplier.`,
},
],
isError: true,
};
}
return { content: [{ type: "text", text: JSON.stringify(supplier) }] };
},
{ annotations: { readOnlyHint: true } },
);
const suppliers = createSdkMcpServer({
name: "suppliers",
version: "1.0.0",
tools: [lookupSupplier],
});
for await (const message of query({
prompt: "Read ./invoices/INV-0042.pdf and check the supplier against our records.",
options: {
mcpServers: { suppliers },
allowedTools: ["Read", "mcp__suppliers__lookup_supplier"],
permissionMode: "dontAsk",
},
})) {
if (message.type === "result" && message.subtype === "success") console.log(message.result);
}The same tool in Python uses the @tool decorator. The schema is a plain dict of names to types, which the SDK converts to JSON Schema. When you need enums, optional fields or nesting, pass a full JSON Schema dict instead.
import json
from typing import Any
from claude_agent_sdk import tool, create_sdk_mcp_server, ToolAnnotations
@tool(
"lookup_supplier",
"Look up a supplier by ABN. Returns the registered name, payment terms and whether we've paid them before.",
{"abn": str},
annotations=ToolAnnotations(readOnlyHint=True),
)
async def lookup_supplier(args: dict[str, Any]) -> dict[str, Any]:
supplier = await db.suppliers.find_by_abn(args["abn"])
if supplier is None:
return {
"content": [{"type": "text", "text": f"No supplier on file with ABN {args['abn']}. Treat this invoice as new-supplier."}],
"is_error": True,
}
return {"content": [{"type": "text", "text": json.dumps(supplier)}]}
suppliers = create_sdk_mcp_server(name="suppliers", version="1.0.0", tools=[lookup_supplier])A few things worth knowing.
Tool names are fully qualified as mcp__<server>__<tool>, where the server segment is the key you used in mcpServers. That full string is what goes in allowedTools. mcp__suppliers__* covers every tool on the server.
Returning isError: true (Python: "is_error": True) is how you write the error message Claude reads. An uncaught exception in the handler doesn't kill the loop either; the SDK catches it and passes the raw message through. But "No supplier on file, treat as new" gives the model something to act on. A stack trace doesn't.
readOnlyHint: true lets Claude call the tool in parallel with other read-only tools. It's a hint, not enforcement. If your handler writes to disk, don't mark it read-only.
Tool search is on by default and defers your SDK tool schemas: Claude sees a compact list of names and loads a schema when it needs one. With a handful of tools it doesn't matter. With forty, it's the difference between a usable context window and not.
One Python-specific limit: the @tool decorator forwards only content and is_error from your return dict. If you need structuredContent, run a standalone MCP server instead.
Connecting external MCP servers
Anything that speaks MCP plugs into the same mcpServers map. Stdio servers are spawned as subprocesses; SSE and HTTP servers are connected to over the network.
mcpServers: {
suppliers, // in-process, from above
postgres: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-postgres", process.env.DATABASE_URL!],
},
xero: {
type: "http",
url: "https://mcp.example.com/xero",
headers: { Authorization: `Bearer ${process.env.XERO_MCP_TOKEN}` },
},
},Treat every external MCP server as an untrusted input source. Its tool results land in Claude's context, which means a poisoned CRM record or a malicious web page can carry instructions your agent might follow. Scope allowedTools to the specific tools you need rather than mcp__xero__*, and put anything that spends money or sends email behind a hook (next section). If you've read my post on the Claude Chrome extension's auto-approve classifier, the same threat model applies, minus the classifier.
Using Agent Skills in the SDK
Skills are the folder-of-instructions feature from Claude Code: a .claude/skills/<name>/SKILL.md file with frontmatter and a body that Claude reads when the skill is relevant. In the invoice agent I keep a gst-rules skill with the edge cases for Australian GST treatment, so the prompt stays short and the rules live in a file I can version.
Three things you need to know for the SDK specifically.
There's no programmatic API for skills. They're discovered from the filesystem through the user and project setting sources. If you've set settingSources explicitly, include "project" (and "user" if the skill lives in ~/.claude/skills). If you left it at the default, they load already.
The skills option controls which ones Claude may invoke: "all", a list of names, or []. When you set skills, the SDK adds the Skill tool to allowedTools for you. If you also pass an explicit tools list, put "Skill" in it, or the tool won't exist and nothing will load.
options = ClaudeAgentOptions(
cwd=os.getcwd(), # .claude/skills/ lives here or in a parent
setting_sources=["project"],
skills=["gst-rules"],
allowed_tools=["Read", "Glob", "mcp__suppliers__lookup_supplier"],
permission_mode="dontAsk",
)To confirm a skill loaded, check the skills array on the system/init message. A skill with user-invocable: false in its frontmatter loads but won't appear in slash_commands, which is easy to misread as a failed load.
Plugins bundle skills, agents, hooks and MCP servers together and load from a local path via the plugins option. CLAUDE.md files load through the same setting sources. If you turn sources off for isolation, you lose CLAUDE.md too, so move anything you still need into systemPrompt.
Permissions, hooks and subagents
This is the section to read twice. An agent that reads invoices is fine. An agent that reads invoices and has Bash is a different risk category, and the SDK's permission model has enough moving parts that a wrong assumption can quietly open a hole.
Permission modes explained
| Mode | What it does |
|---|---|
default |
Nothing is auto-approved by mode. Calls that need approval and match no allow rule go to your canUseTool callback. |
dontAsk |
Anything that would have prompted is denied instead. canUseTool is never called. |
acceptEdits |
File edits and filesystem commands (mkdir, rm, mv, cp, sed) inside the working directory are auto-approved. |
plan |
Read-only exploration. File edits are never auto-approved, even with an allow rule; they go to canUseTool. |
bypassPermissions |
Approves everything that reaches the mode step. Hooks and deny rules still apply. |
auto |
A separate classifier model approves or denies each risky call. Available in both SDKs. |
Modes are one step in a six-step pipeline, and the order matters more than the modes do:
- Hooks run first. A
PreToolUsehook can deny outright or pass the call on. A hook that returnsallowdoes not skip the next two steps. - Deny rules, from
disallowedToolsandsettings.json. A match blocks the call in every mode, includingbypassPermissions. - Ask rules from
settings.json. A match sends the call tocanUseToolfor confirmation, again in every mode. - The permission mode.
- Allow rules, from
allowedToolsandsettings.json. canUseTool, if nothing above resolved it. IndontAskthis step is skipped and the call is denied.
The consequences of that order are where people get hurt.
Auto-approved calls never reach canUseTool. If a tool is approved by an allow rule or by acceptEdits, any check you wrote in the callback is skipped for that tool. The TypeScript SDK emits a CLAUDE_SDK_CAN_USE_TOOL_SHADOWED process warning when your config makes this happen. For a check that has to run on every call, use a PreToolUse hook. Hooks run before everything else, and a hook deny holds even in bypassPermissions.
allowedTools does not constrain bypassPermissions. Listing ["Read"] alongside bypassPermissions still approves Bash, Write and Edit, because unlisted tools fall through to the mode and the mode says yes. If you need bypassPermissions with specific tools blocked, use disallowedTools.
Deny rules are matched as written. Bash(rm *) blocks rm -rf ./tmp and doesn't block /bin/rm -rf ./tmp. Bare Bash removes the tool entirely.
Path anchoring has a double-slash trap. Edit(//secrets/**) blocks writes under /secrets on disk. Edit(/secrets/**) with one slash anchors to the working directory, so it blocks ./secrets and leaves /secrets alone. Not the rule you thought you wrote.
Unanchored allow globs are ignored. allowedTools: ["*"] or ["mcp__*"] does nothing beyond a startup warning. Allow globs need a literal server prefix, like mcp__suppliers__*.
A word on auto mode, since it became the default for interactive Claude Code sessions on Pro, Max and Team plans in August. It works in the SDK too, and the Python type includes it despite what some older writeups claim. Availability depends on your provider and model, and on API-key surfaces the classifier's calls count toward your token usage, so there's a cost line to consider. Check the permission-modes page in the Claude Code docs for the current model floor before you build on it. For an unattended agent I still prefer dontAsk with a tight allow list. It's boring, and boring is the goal.
Subagents inherit the parent's mode unless you set permissionMode on the AgentDefinition and the parent is in default, dontAsk or plan. A subagent never gets bypassPermissions on its own; it runs in that mode only when the parent does. Which means if your parent runs bypassPermissions, every subagent has full system access too, with a different system prompt and whatever behaviour that produces.
Hooks: intercepting the agent loop
Hooks are in-process callbacks that fire at fixed points in the loop. Both SDKs support PreToolUse, PostToolUse, PostToolUseFailure, UserPromptSubmit, Stop, SubagentStart, SubagentStop, PreCompact, PermissionRequest and Notification. TypeScript additionally gets SessionStart, SessionEnd, Setup, PostToolBatch, PermissionDenied and a handful of newer events. In Python, SessionStart and SessionEnd have to be shell hooks in .claude/settings.json, loaded with setting_sources=["project"].
Here's the hook that keeps the invoice agent away from secrets. It matches four tools and denies any call whose input mentions a .env file, which catches Read on .env, cat .env through Bash, and the creative variants in between.
import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
const protectSecrets: HookCallback = async (input) => {
if (input.hook_event_name !== "PreToolUse") return {};
if (/\.env\b/.test(JSON.stringify(input.tool_input))) {
return {
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Secrets are off limits for this agent.",
},
};
}
return {};
};
// in options:
hooks: {
PreToolUse: [{ matcher: "Read|Edit|Write|Bash", hooks: [protectSecrets] }],
},And in Python:
import json, re
from claude_agent_sdk import HookMatcher
async def protect_secrets(input_data, tool_use_id, context):
if re.search(r"\.env\b", json.dumps(input_data["tool_input"])):
return {"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Secrets are off limits for this agent.",
}}
return {}
# in options:
hooks={"PreToolUse": [HookMatcher(matcher="Read|Edit|Write|Bash", hooks=[protect_secrets])]},Stringifying the whole tool input is crude, and that's the point. A scoped deny rule on the file tools handles the polite case; the hook handles Bash and whatever else the model thinks of.
PreToolUse can also return permissionDecision: "allow" with an updatedInput to rewrite the arguments before the tool runs, which is how you'd redirect all writes into a sandbox directory. PostToolUse can append additionalContext to a tool result or replace the output with updatedToolOutput. What PostToolUse cannot do is undo the call. It already ran. Anything protective goes in PreToolUse; PostToolUse is for the audit log, which for an AP agent is not optional. I log tool name, a hash of the input and the session ID on every call.
Hooks defined in settings.json also load when the matching setting source is on. So an SDK app with default settingSources inherits the project's hooks, which is either convenient or alarming depending on whose project it is.
Subagents: delegation and context isolation
Subagents are separate conversations the main agent can spawn through the Agent tool. Each one gets its own context, its own tools and optionally its own model, does its work, and returns only its final message to the parent. The parent's history doesn't leak in, and the subagent's tool calls don't clutter the parent's context.
For the invoice agent, GST checking is a good candidate. It's a distinct job with its own rules, and I'd rather run it on Sonnet than on whatever the parent is using.
options: {
allowedTools: ["Read", "Glob", "Agent", "mcp__suppliers__lookup_supplier"],
agents: {
"gst-checker": {
description: "Verifies the GST treatment on an Australian tax invoice. Use for any invoice that shows a GST line.",
prompt: "You are a GST specialist. Check that GST is 10% of the taxable subtotal, that GST-free items are correctly excluded, and that the document says 'tax invoice'. Report discrepancies only.",
tools: ["Read"],
model: "sonnet",
},
},
},In Python the shape is the same, with one trap: AgentDefinition uses camelCase field names (disallowedTools, permissionMode, maxTurns) because they map to the wire format, while ClaudeAgentOptions uses snake_case. Pass max_turns to an AgentDefinition and you get a TypeError at construction time.
A few things the docs mention only in passing.
The delegation tool shows up as "Agent" in tool_use blocks but "Task" in the system/init tools list, and CLI versions before 2.1.63 used "Task" in both places. If you're watching for delegation, match both.
Programmatic agents override filesystem agents of the same name in .claude/agents/. Handy when you want to ship a corrected version without touching the repo.
A subagent knows nothing you didn't put in the Agent tool's prompt string. No parent history, no parent system prompt. If the GST checker needs the supplier's registration status, the parent has to say so.
Subagents run in parallel, so three independent checks finish in the time of the slowest one. They also bill in parallel. Nested spawning is capped at a depth of 3 by default (CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH, set it to 1 to stop nesting), and concurrency at 20 (CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS). Set maxBudgetUsd before you find out what an unbounded tree of subagents costs.
Taking your agent to production
Structured outputs and sessions
An invoice agent that returns prose is a demo. One that returns typed JSON is a pipeline stage. The SDK's outputFormat option takes a JSON Schema, makes the model conform to it, and puts the validated object on the result message.
import { z } from "zod";
const Invoice = z.object({
invoiceNumber: z.string(),
supplierAbn: z.string(),
supplierName: z.string(),
totalIncGst: z.number(),
dueDate: z.string().describe("ISO 8601 date"),
confidence: z.enum(["high", "medium", "low"]),
});
for await (const message of query({
prompt: "Extract the details from ./invoices/INV-0042.pdf.",
options: {
allowedTools: ["Read", "mcp__suppliers__lookup_supplier"],
mcpServers: { suppliers },
permissionMode: "dontAsk",
outputFormat: { type: "json_schema", schema: z.toJSONSchema(Invoice) }, // Zod 4
},
})) {
if (message.type === "result" && message.subtype === "success") {
const invoice = Invoice.parse(message.structured_output);
await ap.enqueue(invoice); // typed all the way down
}
}In Python, build the schema with Pydantic (Invoice.model_json_schema()), pass output_format={"type": "json_schema", "schema": ...}, and read message.structured_output. The schema support covers the basics: types, enum, const, required, nested objects and $ref. If the model can't produce a conforming object after a few tries you get a result with subtype error_max_structured_output_retries instead of garbage, which is the right failure mode for a pipeline.
Sessions are the other production feature. Every run has a session ID (on the init and result messages), and you can resume one later with history intact, or fork it to try two continuations from the same point. Compaction is automatic when the context gets full. If you turn on enableFileCheckpointing (Python: enable_file_checkpointing), the SDK snapshots files as the agent edits them and you can roll back to any user message's uuid with rewindFiles(). And query.interrupt() stops a running agent from the outside, which you'll want wired to a cancel button before someone asks for it.
Hosting patterns and sandboxes
The SDK runs the agent in your process, so where it runs is your problem. Anthropic's hosting guide describes three patterns, and the names are about what you'd expect.
Ephemeral: one container per task, destroyed when it finishes. This is the invoice agent. A PDF arrives, a container spins up, the agent runs, the JSON comes out, the container dies. Nothing to clean up, nothing to leak between customers.
Long-running: a persistent container per user or per workspace, for agents that hold state across many turns.
Hybrid: a persistent coordinator that spawns ephemeral workers.
For the container itself, the docs list Modal Sandbox, Cloudflare Sandboxes, Daytona, E2B, Fly Machines and Vercel Sandbox, plus Docker, gVisor and Firecracker if you'd rather run your own. Pick whichever one you already run; nothing about the SDK cares.
Two things the docs say quietly that deserve saying loudly. First, the SDK gives you an async generator, not a server. The HTTP or WebSocket layer, thread management, per-user isolation and auth are all yours to build. Second, set settingSources: [] on anything multi-tenant. Otherwise a stray .claude/settings.json in the working directory becomes part of your customer's agent config, hooks included.
Every query() spawns the CLI subprocess, and that startup is measurable, a second or two on a cold container. For a batch job it's noise. For a chat UI it's the difference between snappy and sluggish. The TypeScript SDK has a warm-start path that spins the subprocess up before the first prompt; it's labelled preview, so check the TypeScript reference for the current shape of the API before you depend on it.
For observability, pip install "claude-agent-sdk[otel]" gives you OpenTelemetry tracing, and the SDK propagates TRACEPARENT into the CLI subprocess so the spans join up. If you're on AWS, Bedrock AgentCore will host the SDK as a managed runtime.
Cost control and pricing
You pay per token at the model's API rate, plus your compute. Nothing extra for the SDK itself.
| Model | Model ID | Input / M tokens | Output / M tokens |
|---|---|---|---|
| Claude Fable 5.1 | claude-fable-5-1 |
$10 | $50 |
| Claude Opus 5 | claude-opus-5 |
$5 | $25 |
| Claude Sonnet 5 | claude-sonnet-5 |
$2 | $10 |
| Claude Haiku 4.5 | claude-haiku-4-5 |
$1 | $5 |
Sonnet 5's $2/$10 was announced as introductory pricing through August and was supposed to rise to $3/$15 on 1 September. Anthropic dropped the increase, so $2/$10 is the standard rate now. Prompt cache reads are 10% of the input price, or 2.5% on Fable 5.1, which matters for an agent because the system prompt, tool schemas and CLAUDE.md go out on every turn.
Three levers, in the order I reach for them. maxBudgetUsd (Python: max_budget_usd) caps a single run; the result comes back with subtype error_max_budget_usd when it trips. maxTurns caps the loop. And the model choice per subagent, because most of the work in an agent is not the hard part, and Sonnet at 40% of Opus's price does the easy parts fine.
If you use auto mode, the classifier calls are billed as tokens on API keys. Not huge, but it's a line item.
Auth rules and branding
This one's short because the rule is short. If you're building a product on the Agent SDK, authenticate with an API key from the Console or one of the cloud providers. Anthropic's docs say it directly: unless previously approved, third-party developers may not offer claude.ai login or subscription rate limits in their products, and that includes agents built on the SDK. Using an OAuth token from a Free, Pro or Max account in any other product, the SDK included, is a breach of the consumer terms.
What confuses people is the subscription side. In June, Anthropic announced and then paused a change to how Agent SDK, claude -p and third-party app usage would be metered on subscriptions. As of today nothing changed: that usage still draws from your subscription's limits. That's about you, on your own machine, on your own account. It doesn't turn a Max plan into a way to power a product for other people.
Branding is in the same docs. You can call your agent "Claude Agent", list "Claude" inside an Agents menu, or say "YourAgent, powered by Claude". You can't call it "Claude Code" or "Claude Code Agent", and you can't reuse Claude Code's ASCII art or visual style. Your product keeps its own name.
Using the Agent SDK from Go
My backends are Go, so this is the question I actually cared about. The answer is that the SDK is Python and TypeScript only, and the official route for any other language is to run the Claude Code CLI as a subprocess with -p and --output-format stream-json. The SDKs themselves do exactly this under the hood.
cmd := exec.CommandContext(ctx, "claude", "-p", prompt,
"--output-format", "stream-json", "--verbose",
"--allowedTools", "Read,Glob",
"--permission-mode", "dontAsk",
)
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
sc := bufio.NewScanner(stdout)
sc.Buffer(make([]byte, 0, 1<<20), 16<<20) // tool results can be large
for sc.Scan() {
var ev struct {
Type string `json:"type"`
Subtype string `json:"subtype"`
Result string `json:"result"`
SessionID string `json:"session_id"`
TotalCostUSD float64 `json:"total_cost_usd"`
IsError bool `json:"is_error"`
}
if err := json.Unmarshal(sc.Bytes(), &ev); err != nil {
continue // not every line is an event you care about
}
if ev.Type == "result" {
log.Printf("session=%s cost=$%.4f err=%v", ev.SessionID, ev.TotalCostUSD, ev.IsError)
}
}
return cmd.Wait()stream-json needs --verbose or you get nothing. --include-partial-messages adds token-level deltas if you're streaming to a UI. --input-format stream-json turns it bidirectional for multi-turn. Check the exit code first and the JSON second; a hard failure can print to stderr with a mangled stdout.
There are at least five community Go ports on GitHub (schlunsen, panbanda, ProjAnvil, next-bin and connerohnesorge all have one). They all wrap the CLI the same way, none is affiliated with Anthropic, and every one of them lags a release train that ships daily. For a prototype, pick the one with the most recent commit. For production I wrap the CLI myself. It's about sixty lines, I own every one of them, and when Anthropic renames a field I'm not waiting on a maintainer. One warning: a couple of the ports show OAuth login flows in their READMEs. Re-read the auth section before copying that.
Eight mistakes I'd rather you didn't make
- Installing
@anthropic-ai/claude-codeorclaude-code-sdkand wondering why the imports don't match the docs. Those are the old names. - Expecting
.envto load. It doesn't. Export the key or load the file yourself. - Getting a bland agent and blaming the model. You're on the minimal system prompt. Ask for the
claude_codepreset. - Trusting
allowedToolsunderbypassPermissions. It approves, it doesn't restrict. UsedisallowedToolsfor the things that must never run. - Writing
Edit(/secrets/**)and thinking it covers/secrets. One slash anchors to the working directory. Two slashes for an absolute path. - Putting the safety check in
PostToolUse. The tool already ran.PreToolUseis the only hook that can stop it. - Matching only
"Task"when watching for subagent calls. Current CLIs report"Agent"intool_useblocks. - Shipping a product on a Pro or Max OAuth token. It's a breach of the consumer terms. API keys.
FAQ
Is the Claude Agent SDK free?
The library costs nothing. You pay for the tokens your agent consumes at the model's API rate, plus whatever you run it on.
Can I use the Claude Agent SDK with a Claude Max subscription?
For your own use, on your own machine, yes: subscription usage currently covers the SDK and claude -p, after Anthropic paused the planned billing change in June. For anything you offer to other people, no. Products need API keys.
Does the Claude Agent SDK support Go?
Not officially. Anthropic's answer for other languages is to run the CLI as a subprocess with --output-format stream-json, which is what the section above does. The community Go ports wrap the same thing.
Do I need Claude Code installed to use the SDK?
No. The binary is bundled in the npm package and the Python wheels. The exceptions are npm ci --omit=optional and sdist installs, where you install Claude Code yourself and set the executable path.
Claude Agent SDK or OpenAI Agents SDK?
If your agent needs a computer (files, shell, a codebase) and you're happy on Claude, the Agent SDK gives you Claude Code's loop with nothing to build. If you want to swap models between providers, or you want handoffs and guardrails as first-class primitives, OpenAI's SDK is built around those. Neither is a hosting product; that's Managed Agents and the Agents API respectively, and both of those are US-only today.
Where to go from here
If you'd rather someone else built the agent, that's what I do for a living. I build AI agents and workflow automation for small and medium businesses across Australia, and accounts payable is where most of them start.
Docs I checked against
- Agent SDK overview, quickstart and migration guide
- Custom tools, permissions, hooks, subagents, skills, structured outputs and hosting
- TypeScript reference and Python reference
- Building agents with the Claude Agent SDK, the rename announcement
- Legal and compliance for the auth and branding rules, and pricing
- anthropics/claude-agent-sdk-typescript and anthropics/claude-agent-sdk-python on GitHub
Related Articles
Pi Coding Agent: The SDK Is the Real Reason to Care
I spent a week with the Pi coding agent. The CLI is good, not revolutionary — the TypeScript SDK is what makes it worth switching. Setup, extensions, and a working build.
22 min read
AI CodingHow My Agentic Coding Workflow Changed in a Year
A year ago I wrote four markdown files before starting a coding agent. Now I write three sentences. What changed in agentic coding, and what still needs a real prompt.
11 min read
AI NewsClaude Chrome Extension: The 17% Nobody Is Quoting
Claude in Chrome went GA with auto-approve on by default. Anthropic published the classifier's miss rate. It isn't the number in the press coverage.
10 min read