Claude Agent SDK in Late 2026: What Is New and How to Use It

TL;DR
What changed in the Claude Agent SDK by October 2026: nested subagents with caps, SessionStore, skills, plugins, structured outputs and permission defaults.
Last updated: October 17, 2026
The Claude Agent SDK is Claude Code's agent loop, built-in tools, permissions, sessions and hooks packaged as a TypeScript and Python library that bundles and drives the Claude Code binary. The current releases are @anthropic-ai/claude-agent-sdk 0.3.302 (October 16, 2026) and claude-agent-sdk 0.2.168 for Python (October 15, 2026), and the TypeScript version tracks the CLI release for release. Our verdict: it is the most complete way to embed a coding-capable agent in a process you operate, provided you accept three things - it runs Claude models only, products you ship need API-key auth and cannot offer claude.ai login without Anthropic's approval, and several defaults (permission mode, background subagents, nested and fork subagents) changed this year, so pin versions and set those options explicitly.
This is a feature tour of what the SDK is now, with the release that added each piece, the changed default to watch for, and code type-checked against the current packages. If you are still deciding whether to build on the SDK or just drive the CLI, start with Claude Agent SDK vs Claude Code and come back.
Official Sources#
| Resource | What it confirms |
|---|---|
| Agent SDK overview | What the SDK is, the comparison table, login and branding rules |
| Quickstart | Runtimes, install commands, auth env vars |
| Subagents | AgentDefinition, background default, depth/concurrency/spend caps, Workflow tool |
| Hooks | Events per SDK, defer, decision priority |
| Permissions | Evaluation order, the six modes, the omitted-mode behavior |
| Sessions and Session storage | continue/resume/fork, SessionStore contract and behavior |
| Skills and Plugins | The skills option and its limits, dispatch by name, local plugins |
| Structured outputs | draft-07 validation, retries, startup errors |
| TypeScript CHANGELOG and Python CHANGELOG | Which release added what |
How the SDK relates to Claude Code#
Anthropic's overview describes the Agent SDK as "a library that runs the Claude Code binary". Both SDKs bundle a native Claude Code binary, so most installs need nothing else. The one exception the quickstart calls out: if pip installs the Python source distribution instead of a platform wheel (ARM64 Windows, for example), there is no bundled binary and the SDK looks for a native Claude Code install on your PATH.
Recent TypeScript releases end with "Updated to parity with Claude Code v2.1.N", and since 0.3.x the SDK patch number matches the CLI version. The 0.3.302 release matches Claude Code v2.1.302, and Python 0.2.168 bundles CLI 2.1.301. Not every changelog entry carries the parity line, but in practice a Claude Code changelog entry usually tells you what your SDK agent just gained.
Configuration loading follows the CLI too. When you omit settingSources, query() reads user, project and local settings, CLAUDE.md files, and .claude/ skills, agents and commands, exactly as the terminal does. Pass settingSources: [] to limit the agent to what you configure in code. Managed policy settings and ~/.claude.json are read either way, which is why the docs say not to rely on default options for multi-tenant isolation.
Anthropic's own comparison puts four options side by side: the Agent SDK (embed Claude Code's agent in a process you operate), the Claude Code CLI (interactive use), the Client SDK (call the API and write the tool loop yourself, or use the beta tool runner), and Managed Agents (an Anthropic-hosted harness). Other languages can drive the same loop with claude -p --output-format json.
%% title: How the Agent SDK runs
flowchart LR
A[Your app] -->|query options| B[Agent SDK]
B -->|spawns| C[Claude Code binary]
C --> D[Claude API]
C --> E[Built-in tools]
C --> F[MCP servers]
B -.->|callbacks| G[Hooks and canUseTool]
C -->|JSONL| H[(Local transcript)]
B -.->|mirror| I[(SessionStore)]What changed in 2026 at a glance#
| Area | What is new | Since |
|---|---|---|
| Subagents | Run in the background by default | Claude Code v2.1.198 |
| Subagents | Subagents can spawn their own subagents; the documented depth (3), concurrency (20) and spend caps apply from | TS 0.3.219 / Py 0.2.127 |
| Subagents | Fork subagents (subagent_type: "fork") on by default in SDK sessions | TS 0.3.302 |
| Orchestration | Workflow tool for dozens to hundreds of agents | TS 0.3.149 |
| Sessions | SessionStore mirrors transcripts to your own backend (alpha in TS) | TS 0.2.113 / Py 0.1.64 |
| Skills | Top-level skills option | TS 0.2.120 / Py 0.1.62 |
| Hooks | updatedToolOutput for any tool; defer decision in Python | TS 0.2.121 / Py 0.1.74 |
| Hooks | updatedInput validated like Claude's own input | TS 0.3.300 |
| Permissions | Omitted permissionMode left to Claude Code, then refined | TS 0.3.286, 0.3.302 |
| MCP and plugins | Per-tool permission_policy on sdk MCP servers; reloadPlugins({ addPlugins }) | TS 0.3.301 |
| Structured outputs | Invalid schemas fail at startup; format accepted as annotation | Claude Code v2.1.205 |
| Startup | startup() pre-warm; prewarm() and SpareProcess.claim() (alpha) | TS 0.2.89 / 0.3.282 |
The dates and versions come from the two changelogs and the docs pages above. The rest of this post walks through each row.
Prerequisites and install#
You need Node.js 18+ or Python 3.10+ (PyPI lists requires_python >=3.10).
# TypeScript
npm install @anthropic-ai/claude-agent-sdk
# Python
uv add claude-agent-sdk
# or
pip install claude-agent-sdk
Auth is ANTHROPIC_API_KEY, or one of the provider switches from the quickstart: CLAUDE_CODE_USE_BEDROCK=1, CLAUDE_CODE_USE_ANTHROPIC_AWS=1 (with ANTHROPIC_AWS_WORKSPACE_ID), CLAUDE_CODE_USE_VERTEX=1 or CLAUDE_CODE_USE_FOUNDRY=1, plus that cloud's credentials. The SDK does not load .env files on its own, so load them yourself before calling it.
One rule matters for anything you ship: unless previously approved, Anthropic does not allow third-party developers to offer claude.ai login or rate limits in products built on the Agent SDK. Use API-key auth. The branding guidelines allow "Claude Agent" or "{YourAgentName} Powered by Claude", but not "Claude Code" or "Claude Code Agent". How SDK runs are billed on subscription plans has moved around this year; the SDK vs Claude Code post tracks the current state.
Step 1: Subagents, now nested and capped#
Subagents are defined in code with the agents option. Each AgentDefinition needs a description and a prompt, and the optional fields have grown this year:
tools/disallowedTools- scope what the subagent can callmodel- an alias ('fable','opus','sonnet','haiku','inherit') or a full model IDeffort-'low' | 'medium' | 'high' | 'xhigh' | 'max'or a numbermaxTurns,skills,memory,mcpServers,initialPrompt,background,permissionModeomitClaudeMd(TS 0.3.271+) - start without user, project and localCLAUDE.mdautoCompactWindow(TS 0.3.296+) - compact the subagent earlier than the main window
The last two are TypeScript only; the Python AgentDefinition does not have them.
Three behavior changes are worth knowing before you ship:
- Background is the default. An Agent call that omits
run_in_backgroundlaunches a background subagent, and Claude setsrun_in_background: falsewhen it needs the result first. Before Claude Code v2.1.198 this default was still rolling out, so older pins could run subagents synchronously. Setbackground: trueon a definition to force it. - Subagents nest. A subagent can spawn its own subagents. The subagents doc describes three caps for TS 0.3.219+ and Python 0.2.127+, and warns that on earlier releases some of these limits are missing or default differently:
CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH(default 3 layers;1turns nesting off),CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS(default 20, returnsConcurrent subagent limit reached), andmaxBudgetUsd/max_budget_usd(no default, counts subagent spend, ends the run witherror_max_budget_usd). - Fork subagents are on. As of TS 0.3.302, fork subagents (which inherit the parent conversation instead of starting fresh) are on by default in SDK sessions, as in the terminal.
CLAUDE_CODE_FORK_SUBAGENT=0turns them off.
Two more details trip people up. The TypeScript env option replaces the subprocess environment, so spread process.env or you lose PATH; the Python SDK merges env into the inherited environment. And permission inheritance: a subagent runs in the parent's mode unless its permissionMode is set and the parent is in default, dontAsk or plan. Even then, Claude Code never applies a subagent bypassPermissions value (Claude Code v2.1.267+). A subagent only bypasses permissions when the parent does.
Model choice affects delegation as well. The docs note that Claude Opus 5 delegates to subagents more readily. With the claude_code system prompt preset on Opus 5, Claude Code adds a line telling Claude not to call the Agent tool unless asked; a custom system prompt, or no systemPrompt at all (the SDK default), does not get that line, so add your own instruction and set the caps either way.
Here is the core of a review agent with a scoped subagent and all three caps:
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Use the security-reviewer agent to review the diff in src/auth.",
options: {
permissionMode: "dontAsk", // set it yourself; the omitted default changed in 0.3.286
allowedTools: ["Read", "Grep", "Glob", "Agent"],
agents: {
"security-reviewer": {
description: "Reviews code changes for auth and injection bugs.",
prompt: "You are a security reviewer. Report concrete findings with file and line.",
tools: ["Read", "Grep", "Glob"],
model: "sonnet",
effort: "high",
maxTurns: 20,
omitClaudeMd: true, // TS 0.3.271+
},
},
// TS env REPLACES the subprocess environment, so spread process.env
env: {
...process.env,
CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH: "2",
CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS: "5",
},
maxBudgetUsd: 2, // includes subagent spend
},
})) {
if (message.type === "result" && message.subtype !== "success") {
console.error(message.subtype, message.errors); // e.g. error_max_budget_usd
}
}
%% title: Subagent tree and its caps
flowchart TD
M[Main agent] --> S1[Subagent L1]
M --> S2[Subagent L1]
S1 --> N1[Subagent L2]
N1 --> N2[Subagent L3]
N2 -.->|depth cap| W[Does the work itself]
M -.->|max 20 running| C[Concurrency cap]
M -.->|maxBudgetUsd| B[error_max_budget_usd]When to step up to the Workflow tool#
Subagents suit a few delegated tasks per turn. For runs that coordinate dozens to hundreds of agents, the Workflow tool (TS Agent SDK v0.3.149+) moves the orchestration into a script the runtime executes outside the conversation. A run allows up to 16 concurrent agents by default (adjustable from 1 to 256 with CLAUDE_CODE_WORKFLOW_MAX_CONCURRENT_AGENTS, Claude Code v2.1.269+) and 1,000 agents in total. The SDK never shows the interactive workflow prompt; the call goes through normal permission evaluation, so add Workflow to allowedTools or approve it in canUseTool. The dynamic workflows guide covers the script side.
Step 2: Hooks as in-process guardrails#
SDK hooks are callbacks in your own process, passed in options.hooks with a matcher. Both SDKs support PreToolUse, PostToolUse, PostToolUseFailure, UserPromptSubmit, Stop, SubagentStart, SubagentStop, PreCompact, PermissionRequest and Notification. TypeScript adds about two dozen more, covering session lifecycle (SessionStart, SessionEnd, Setup), compaction and model switches (PostCompact, PreModelSwitch, PostModelSwitch), tool batches, permission denials, tasks and teammates, MCP elicitation, config and instruction loading, and worktree and file-system changes.
What a PreToolUse hook can return has grown:
permissionDecisioncan beallow,deny,askordefer.deferends the turn withstop_reason: "tool_deferred"so your app can resume the call later (Python gained this in 0.1.74, matching TypeScript).- When several hooks disagree,
denybeatsdefer,deferbeatsask, andaskbeatsallow. updatedInputrewrites the tool input. Since TS 0.3.300 it is validated like Claude's own input and unknown keys are dropped, so a hook that injected extra fields will now see them silently removed.canUseToolis unchanged.
On the other side, a PostToolUse hook can set updatedToolOutput to replace any tool's output before Claude sees it (TS 0.2.121, Python 0.1.74). The older updatedMCPToolOutput only covered MCP tools and is deprecated.
A guardrail that blocks writes to .env files, in TypeScript:
import { type HookCallback, type PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
const protectEnv: HookCallback = async (input) => {
const pre = input as PreToolUseHookInput;
const toolInput = pre.tool_input as { file_path?: string };
if (toolInput.file_path?.split("/").pop()?.toLowerCase() === ".env") {
return {
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Agents may not modify .env files",
},
};
}
return {};
};
// options.hooks:
// { PreToolUse: [{ matcher: "Write|Edit", hooks: [protectEnv] }] }
And the same in Python:
from typing import Any
from claude_agent_sdk import HookContext, HookMatcher
async def protect_env(
input_data: Any, tool_use_id: str | None, context: HookContext
) -> Any:
file_path = input_data["tool_input"].get("file_path", "")
if file_path.split("/")[-1].lower() == ".env":
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Agents may not modify .env files",
}
}
return {}
# ClaudeAgentOptions(hooks={"PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env])]})
Hooks run before every other permission step, and a hook deny applies even in bypassPermissions, which makes them the right home for checks that must never be skipped.
Step 3: Permissions and the new default mode#
The SDK evaluates every tool request in a fixed order: hooks, then deny rules, then ask rules, then the permission mode, then allow rules, then your canUseTool callback. Three consequences from the permissions doc:
- A deny rule blocks even in
bypassPermissions. - A hook that returns
allowdoes not skip the deny and ask rules. - Calls auto-approved earlier never reach
canUseTool. The TypeScript SDK emits aCLAUDE_SDK_CAN_USE_TOOL_SHADOWEDprocess warning when your config makes that likely.
%% title: Permission evaluation order
flowchart TD
R[Tool request] --> H{Hooks}
H -->|deny| X[Blocked]
H -->|pass| D{Deny rules}
D -->|match| X
D -->|no match| K{Ask rules}
K -->|match| C{canUseTool}
K -->|no match| M{Permission mode}
M -->|approves| OK[Execute]
M -->|falls through| AL{Allow rules}
AL -->|match| OK
AL -->|no match| C
C -->|allow| OK
C -->|deny| XThere are six modes: default, dontAsk (deny instead of prompting; canUseTool is never called), acceptEdits, bypassPermissions, plan, and auto (a model classifier reviews actions). One warning the docs repeat: allowedTools does not constrain bypassPermissions. allowedTools: ["Read"] with bypassPermissions still approves Bash, Write and Edit. To block tools in that mode, use disallowedTools.
The default changed twice this autumn. Before TS 0.3.286, omitting permissionMode meant default. From 0.3.286 (September 30, 2026) it is left to Claude Code, so a permissions.defaultMode in settings applies, and on third-party providers or with telemetry off the session started in auto mode, like claude -p. Then 0.3.302 (October 16) changed an omitted mode to start in default on third-party providers, with telemetry off, or on a fresh install. A session that starts in auto mode also drops broad allow rules such as a bare Bash entry. The practical advice is short: set permissionMode yourself, every time.
For a locked-down agent, the docs pair allowedTools with permissionMode: "dontAsk". Listed tools run, and anything that would prompt is denied instead. Calls that need no approval in default mode, such as file reads inside your working directory, still run. Remove a tool from the request entirely by putting its bare name in disallowedTools.
TS 0.3.301 added per-tool policy for in-process MCP servers: a tools list of { name, permission_policy } entries on an sdk-type server config, where always_ask forces your host to approve each call (even when allowedTools covers it) and always_deny blocks it. The same release exported BUILTIN_TOOL_NAMES and LEGACY_TOOL_NAME_ALIASES so you can validate rule strings against the real tool names.
Step 4: Sessions that survive a host#
There are three ways back into a conversation:
- continue -
continue: true(TS) orcontinue_conversation=True(Python) resumes the most recent session in the directory. - resume - pass the session ID from any result message's
session_id. - fork -
resumeplusforkSession: true/fork_session=Truebranches into a new session ID and leaves the original intact.
persistSession: false (TypeScript only) keeps a session in memory. Keep one limit in mind: sessions persist the conversation, not the filesystem. A forked agent that edits files edits the real files. Use file checkpointing when you need to roll file changes back. Helpers such as listSessions, getSessionMessages, getSessionInfo, renameSession, tagSession and deleteSession manage what is on disk.
SessionStore is the 2026 addition that matters for anyone running agents on more than one machine. It arrived in TS 0.2.113 (April 17, alpha) and Python 0.1.64 (April 20), and mirrors the JSONL transcripts to a backend you own so another host can resume them. Only append and load are required; listSessions, listSessionSummaries, delete and listSubkeys are optional. InMemorySessionStore is the reference implementation, and the Python repo ships copy-in S3, Redis and Postgres adapters under examples/session_stores/.
The behavior notes are worth reading in full. The short version:
- Dual write. Claude Code writes each batch locally first, then the SDK forwards it to
append(). A store outage does not stop the agent. - Best-effort mirror. A rejected
appendis retried for up to three attempts in total, then the SDK emits asystemmessage with subtypemirror_errorand drops the batch. Retries can re-deliver entries, so dedupe byentry.uuid. - Conflicts.
persistSession: falseand file checkpointing both throw at startup when combined with a store. - Keys follow the cwd.
projectKeyencodes the working directory, so resume from a matching cwd. - Retention is yours. The SDK never deletes from your store.
import { query, InMemorySessionStore } from "@anthropic-ai/claude-agent-sdk";
// Swap for your own adapter (S3, Redis, Postgres...) in production.
// Only append() and load() are required.
const sessionStore = new InMemorySessionStore();
async function run(prompt: string, extra: { resume?: string; forkSession?: boolean } = {}) {
let sessionId: string | undefined;
for await (const message of query({
prompt,
options: {
cwd: "/srv/app", // projectKey is derived from cwd: resume from the same directory
permissionMode: "default",
allowedTools: ["Read", "Grep"],
sessionStore,
...extra,
},
})) {
if (message.type === "system" && message.subtype === "mirror_error") {
console.warn("store append failed, batch dropped:", message);
}
if (message.type === "result") sessionId = message.session_id;
}
return sessionId;
}
const original = await run("Summarize how auth tokens are issued in this repo.");
if (original) {
// Branch the conversation without touching the original history
const fork = await run("Now sketch an OAuth2 migration.", { resume: original, forkSession: true });
// Carry on with the original thread, possibly on another host sharing the store
await run("List the token expiry settings.", { resume: original });
console.log({ original, fork });
}
%% title: Resume from a SessionStore
sequenceDiagram
participant App
participant SDK
participant CC as Claude Code
participant Store as SessionStore
App->>SDK: query with resume and sessionStore
SDK->>Store: load(key)
Store-->>SDK: transcript entries
SDK->>CC: spawn with temp config dir
CC-->>SDK: new transcript batches
SDK->>Store: append(key, entries)
SDK-->>App: messages and resultWhen a run resumes from the store, the SDK writes the transcript into a temporary config directory, points the subprocess at it, and deletes it at the end. The store then holds the only durable copy of that run.
Step 5: Skills and plugins#
Skills are SKILL.md files on disk. There is no programmatic registration API; the SDK discovers them through your setting sources, the same way the CLI does. What changed is control: the skills option (TS 0.2.120, Python 0.1.62) takes a list of names, 'all', or []. Names match the skill's name field or directory name, and plugin skills use the plugin:skill form. Skills left off the list are hidden from the model and the Skill tool refuses them, but the list is not a sandbox. Sending /<name> still runs an unlisted user-invocable skill, and its files stay readable through Read and Bash. If you need a hard boundary, control which skills are on disk through settingSources and plugins, and scope Read and Bash with permissions. Setting the option also adds the Skill tool to allowedTools automatically; if you pass an explicit tools list, include "Skill" yourself.
Plugins load with plugins: [{ type: 'local', path }], and 'local' is the only type the SDK accepts. A plugin can bundle skills, agents, hooks and MCP servers, and its skills are invoked as /plugin-name:skill-name. To use a marketplace plugin, download it and point at the folder. reloadPlugins() (TS 0.2.85) refreshes plugins from disk, and since 0.3.301 it accepts addPlugins to load more folders into a running session.
import { query } from "@anthropic-ai/claude-agent-sdk";
import path from "node:path";
const q = query({
prompt: "/release-tools:changelog draft notes for the last 10 commits",
options: {
cwd: process.cwd(),
settingSources: ["project"], // discover .claude/skills in this repo only
// the model may invoke only these (plugin skills use plugin:skill); /name dispatch is not restricted
skills: ["release-tools:changelog", "security-check"],
plugins: [{ type: "local", path: path.resolve("./plugins/release-tools") }],
permissionMode: "default",
allowedTools: ["Read", "Grep", "Glob", "Bash(git log *)"],
},
});
for await (const message of q) {
if (message.type === "system" && message.subtype === "init") {
console.log("skills:", message.skills);
console.log("plugins:", message.plugins);
// Load another plugin folder into the running session (TS 0.3.301+)
await q.reloadPlugins({
addPlugins: [{ type: "local", path: path.resolve("./plugins/extra") }],
});
}
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}
If Claude tries to invoke a skill that is not on the list, the Skill tool returns Skill <name> is not in this session's skills allowlist. Add the name, or dispatch the skill yourself with /<name>.
If you are unsure whether a piece of behavior belongs in a skill or a subagent, Claude agents vs skills lays out the split: skills standardize work in the current context, subagents isolate it.
Step 6: Custom tools, MCP and tool search#
Custom tools are defined with tool() in TypeScript (Zod input schema) or @tool in Python, then wrapped in an in-process MCP server with createSdkMcpServer / create_sdk_mcp_server. Each tool's full name is mcp__<server>__<tool>, where the server segment is the key you use in mcpServers. List that name in allowedTools to skip the prompt.
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
const semverBump = tool(
"semver_bump",
"Return the next semantic version for a release type",
{
current: z.string().describe("Current version, for example 1.4.2"),
release: z.enum(["major", "minor", "patch"]),
},
async ({ current, release }) => {
const [maj, min, pat] = current.split(".").map(Number);
const next =
release === "major" ? `${maj + 1}.0.0`
: release === "minor" ? `${maj}.${min + 1}.0`
: `${maj}.${min}.${pat + 1}`;
return { content: [{ type: "text", text: next }] };
},
);
const releaseServer = createSdkMcpServer({
name: "release",
version: "1.0.0",
tools: [semverBump],
});
for await (const message of query({
prompt: "We are on 2.3.9 and shipping a new feature. What is the next version?",
options: {
mcpServers: { release: releaseServer },
allowedTools: ["mcp__release__semver_bump"], // mcp__<server>__<tool>
permissionMode: "dontAsk",
},
})) {
if (message.type === "result" && message.subtype === "success") console.log(message.result);
}
MCP tool search is on by default. Tool definitions are withheld from the context window, the agent sees a summary, and up to five relevant tools are loaded on demand. That keeps a large MCP fleet from eating your context. The SDK turns it off when ANTHROPIC_BASE_URL points to a non-first-party host, because most proxies do not forward tool_reference blocks; set ENABLE_TOOL_SEARCH=true to keep it on behind a proxy that supports them. On the Python side, 0.2.140 (August 18) added support for mcp 2.x in in-process servers.
Cutting startup latency#
Spawning the Claude Code subprocess is a fixed cost on every new session. TS 0.2.89 added startup(), which pre-warms the subprocess before the first query; the changelog puts the first query at about 20x faster. TS 0.3.282 added prewarm() and SpareProcess.claim(), both marked alpha. prewarm() starts a parked process before you know which session it will serve, loading config, auth, tools, plugins and MCP handshakes up front, and claim() later binds it to a working directory and sends the first prompt. The type definitions warn that a claim can be refused (for example, by a folder whose project settings set env or model), in which case you fall back to a normal query(), and that options such as hooks, skills and mcpServers are fixed at prewarm() time.
Step 7: Structured outputs you can trust#
Pass outputFormat (TS) or output_format (Python) as { type: 'json_schema', schema }, then read structured_output on the success result. The SDK validates against JSON Schema draft-07 and re-prompts when the output does not match; if retries run out, the result subtype is error_max_structured_output_retries.
The details that changed:
- Zod 4 needs a target. Zod emits draft 2020-12 by default, so use
z.toJSONSchema(schema, { target: "draft-7" }). Pydantic'smodel_json_schema()works as is. - Invalid schemas fail loudly. Since Claude Code v2.1.205 an invalid schema fails the run at startup. Before that it was silently ignored and you got plain text back.
formatis an annotation."format": "email"is accepted but not enforced. Validate it yourself if it matters.- Refusals are errors. TS 0.3.302 fixed a bug where a refused API response could arrive as
structured_output. It now returns an error with no payload.
In TypeScript, the schema and result handling from the review agent look like this:
import { z } from "zod";
const Review = z.object({
verdict: z.enum(["approve", "request_changes"]),
findings: z.array(z.object({ file: z.string(), line: z.number(), issue: z.string() })),
});
// options.outputFormat:
// { type: "json_schema", schema: z.toJSONSchema(Review, { target: "draft-7" }) }
// in the message loop:
// if (message.type === "result" && message.subtype === "success") {
// const review = Review.parse(message.structured_output);
// }
And in Python with Pydantic:
from typing import Literal
from pydantic import BaseModel
class Finding(BaseModel):
file: str
line: int
issue: str
class Review(BaseModel):
verdict: Literal["approve", "request_changes"]
findings: list[Finding]
# ClaudeAgentOptions(output_format={"type": "json_schema", "schema": Review.model_json_schema()})
# if message.subtype == "success" and message.structured_output:
# review = Review.model_validate(message.structured_output)
Parsing the output again on your side is cheap insurance. It also gives you typed data without a cast.
Verify it works#
What we checked: every TypeScript snippet in this post, with the commented-out wiring filled in, was type-checked against @anthropic-ai/claude-agent-sdk@0.3.302 and zod@4.6.7 with typescript@7.0.2, strict mode and moduleResolution: "bundler"; npx tsc --noEmit passed, and a deliberately wrong permission mode string was rejected. The Python snippets were checked against claude-agent-sdk==0.2.168 with Pydantic on Python 3.13: py_compile and an import check passed, with ClaudeAgentOptions constructed using the hook and the output schema. We did not make API calls, so we have not observed live output.
On a real run, look for three things:
- The first
systemmessage with subtypeinitlists theskillsandpluginsthe session loaded. If a skill is missing there, it is a discovery problem (setting sources or path), not a prompt problem. - A finished run ends with a
resultmessage.subtype: "success"plus a populatedstructured_outputmeans the schema path worked. - When a cap is hit, the result subtype tells you which one:
error_max_budget_usd,error_max_turnsorerror_max_structured_output_retries.
Troubleshooting#
Not logged in when resuming from a store. On resume, the Python SDK copies only credentials and .claude.json into the temporary config directory, so an apiKeyHelper in your user settings.json is not available. Set ANTHROPIC_API_KEY in env, or move the helper into managed or project settings. The TypeScript SDK also copies user settings.json (since 0.3.222).
canUseTool never fires. Something earlier approved the call: a bare allowedTools entry, acceptEdits, or bypassPermissions. Watch for the CLAUDE_SDK_CAN_USE_TOOL_SHADOWED warning, and move checks that must always run into a PreToolUse hook.
Commands fail with "not found" after setting env. In TypeScript, env replaces the subprocess environment. Spread process.env into it.
A skill says it is not in the allowlist. Check the name in your skills list; plugin skills need the plugin:skill form. Sending /<name> in the prompt runs a user-invocable skill without listing it.
The structured output schema is rejected. Your Zod schema is probably draft 2020-12. Convert with { target: "draft-7" }. Since v2.1.205 an invalid schema fails at startup instead of being ignored.
Broad allow rules stopped working. The session most likely started in auto mode, which drops rules such as a bare Bash. Pass permissionMode explicitly.
Too many subagents, or a surprise bill. Set maxBudgetUsd, lower CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS, and set CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH=1 if you do not want nesting at all.
Where it fits next to other frameworks#
LangGraph gives you explicit graph state, durable checkpointing and free choice of model provider, and the OpenAI Agents SDK has a clean handoff primitive for passing control between agents. The Claude Agent SDK's strength is that you get the full Claude Code harness, with built-in file and shell tools, permissions, skills and parity with the CLI, in one dependency; the tradeoff is that it runs Claude models only. For the detailed comparisons, see Claude Agent SDK vs LangGraph and OpenAI Agents SDK vs Claude Agent SDK.
FAQ#
Is the Claude Agent SDK the same as Claude Code?#
It runs the same thing. The SDK is a library that bundles and drives the Claude Code binary, so you get the same agent loop, built-in tools, permissions, sessions and hooks. Recent TypeScript releases name the Claude Code version they match: 0.3.302's changelog states parity with Claude Code v2.1.302.
Can Claude Agent SDK subagents spawn their own subagents?#
Yes. Subagents can spawn their own subagents; the documented depth (3 layers below the main agent), concurrency (20) and spend caps apply from TypeScript SDK 0.3.219 and Python SDK 0.2.127. Set CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH=1 in env to turn nesting off, and use CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS and maxBudgetUsd to bound the rest.
What permission mode does the SDK use if I do not set one?#
It depends on your settings and provider. Since TS 0.3.286 an omitted permissionMode is left to Claude Code, so a settings defaultMode applies and some setups start in auto mode; since 0.3.302, third-party providers, telemetry-off setups and fresh installs start in default. If your app depends on the mode, pass it explicitly.
How do I resume an Agent SDK session on another server?#
Give both hosts the same SessionStore adapter (only append and load are required) and resume from a working directory that matches the original run, because the store key is derived from the cwd. Without a store, you would have to move the local JSONL transcript yourself.
Does structured output work with Zod 4?#
Yes. Convert with z.toJSONSchema(schema, { target: "draft-7" }), because the SDK validates against draft-07 and Zod 4 defaults to draft 2020-12. Pass the result as outputFormat.schema.
Sources#
- Agent SDK overview: https://code.claude.com/docs/en/agent-sdk/overview
- Quickstart: https://code.claude.com/docs/en/agent-sdk/quickstart
- Subagents: https://code.claude.com/docs/en/agent-sdk/subagents
- Hooks: https://code.claude.com/docs/en/agent-sdk/hooks
- Permissions: https://code.claude.com/docs/en/agent-sdk/permissions
- Sessions: https://code.claude.com/docs/en/agent-sdk/sessions
- Session storage: https://code.claude.com/docs/en/agent-sdk/session-storage
- Skills: https://code.claude.com/docs/en/agent-sdk/skills
- Plugins: https://code.claude.com/docs/en/agent-sdk/plugins
- Custom tools: https://code.claude.com/docs/en/agent-sdk/custom-tools
- MCP: https://code.claude.com/docs/en/agent-sdk/mcp
- Tool search: https://code.claude.com/docs/en/agent-sdk/tool-search
- Structured outputs: https://code.claude.com/docs/en/agent-sdk/structured-outputs
- Claude Code features in the SDK: https://code.claude.com/docs/en/agent-sdk/claude-code-features
- Dynamic workflows: https://code.claude.com/docs/en/workflows
- TypeScript SDK changelog: https://github.com/anthropics/claude-agent-sdk-typescript/blob/main/CHANGELOG.md
- Python SDK changelog: https://github.com/anthropics/claude-agent-sdk-python/blob/main/CHANGELOG.md
- npm package: https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk
- PyPI package: https://pypi.org/project/claude-agent-sdk/
Continue Reading#
- Claude Agent SDK vs Claude Code: When to Build and When to Drive - the build-vs-drive decision, including Managed Agents and billing
- Claude Code Dynamic Workflows: The Complete Guide - what to reach for when subagents are not enough
- Claude Agent SDK vs LangGraph: Choosing Your Agent Stack in 2026 - sessions vs checkpoints, and when each stack fits
- Human-in-the-Loop Patterns for AI Agents: Approvals in 5 SDKs - where
canUseTooland hooks sit among five SDKs' approval models - Context Engineering for Agents: Caching, Compaction, Subagents - compaction, tool-result clearing and subagents across Claude and OpenAI
Get the next deep dive like this in your inbox
One email a week on ai-agents and the rest of the AI dev stack. Free.
Read next on Claude Code
Claude Agent SDK vs Claude Code: When to Build and When to Drive
Claude Agent SDK vs Claude Code explained: same engine, two surfaces. Here is the concrete decision line, plus where Managed Agents fits as the hosted third option.
8 min readClaude Agent SDK vs LangGraph: Choosing Your Agent Stack in 2026
Claude Agent SDK vs LangGraph head-to-head: architecture, state handling, multi-agent patterns, and real pricing - plus a decision guide for which agent stack fits your team in 2026.
9 min readOpenAI Agents SDK vs Claude Agent SDK: Building Agents on the Two Big Platforms
OpenAI Agents SDK vs Claude Agent SDK compared - handoffs vs subagents, guardrails vs hooks, sandboxing, MCP support and how to choose a platform for your team.
9 min readTechnical content at the intersection of AI and development. Building with AI agents, Claude Code, and modern dev tools - then showing you exactly how it works.








