claude-agent-sdk
Claude Agent SDK - Structured Outputs & Error Prevention Guide
Package: @anthropic-ai/claude-agent-sdk@0.2.12 Breaking Changes: v0.1.45 - Structured outputs (Nov 2025), v0.1.0 - No default system prompt, settingSources required
What's New in v0.1.45+ (Nov 2025)
Major Features:
1. Structured Outputs (v0.1.45, Nov 14, 2025)
- JSON schema validation - Guarantees responses match exact schemas
outputFormatparameter - Define output structure with JSON schema or Zod- Access validated results - Via
message.structured_output - Beta header required:
structured-outputs-2025-11-13 - Type safety - Full TypeScript inference with Zod schemas
Example:
import { query } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
const schema = z.object({
summary: z.string(),
sentiment: z.enum(['positive', 'neutral', 'negative']),
confidence: z.number().min(0).max(1)
});
const response = query({
prompt: "Analyze this code review feedback",
options: {
model: "claude-sonnet-4-5",
outputFormat: {
type: "json_schema",
json_schema: {
name: "AnalysisResult",
strict: true,
schema: zodToJsonSchema(schema)
}
}
}
});
for await (const message of response) {
if (message.type === 'result' && message.structured_output) {
// Guaranteed to match schema
const validated = schema.parse(message.structured_output);
console.log(`Sentiment: ${validated.sentiment}`);
}
}
Zod Compatibility (v0.1.71+): SDK supports both Zod v3.24.1+ and Zod v4.0.0+ as peer dependencies. Import remains import { z } from "zod" for either version.
2. Plugins System (v0.1.27)
pluginsarray - Load local plugin paths- Custom plugin support - Extend agent capabilities
3. Hooks System (v0.1.0+)
All 12 Hook Events:
| Hook | When Fired | Use Case |
|---|---|---|
PreToolUse |
Before tool execution | Validate, modify, or block tool calls |
PostToolUse |
After tool execution | Log results, trigger side effects |
Notification |
Agent notifications | Display status updates |
UserPromptSubmit |
User prompt received | Pre-process or validate input |
SubagentStart |
Subagent spawned | Track delegation, log context |
SubagentStop |
Subagent completed | Aggregate results, cleanup |
PreCompact |
Before context compaction | Save state before truncation |
PermissionRequest |
Permission needed | Custom approval workflows |
Stop |
Agent stopping | Cleanup, final logging |
SessionStart |
Session begins | Initialize state |
SessionEnd |
Session ends | Persist state, cleanup |
Error |
Error occurred | Custom error handling |
Hook Configuration:
const response = query({
prompt: "...",
options: {
hooks: {
PreToolUse: async (input) => {
console.log(`Tool: ${input.toolName}`);
return { allow: true }; // or { allow: false, message: "..." }
},
PostToolUse: async (input) => {
await logToolUsage(input.toolName, input.result);
}
}
}
});
4. Additional Options
fallbackModel- Automatic model fallback on failuresmaxThinkingTokens- Control extended thinking budgetstrictMcpConfig- Strict MCP configuration validationcontinue- Resume with new prompt (differs fromresume)permissionMode: 'plan'- New permission mode for planning workflows
š Docs: https://platform.claude.com/docs/en/agent-sdk/structured-outputs
The Complete Claude Agent SDK Reference
Table of Contents
- Core Query API
- Tool Integration
- MCP Servers
- Subagent Orchestration
- Session Management
- Permission Control
- Sandbox Settings
- File Checkpointing
- Filesystem Settings
- Query Object Methods
- Message Types & Streaming
- Error Handling
- Known Issues
Core Query API
Key signature:
query(prompt: string | AsyncIterable<SDKUserMessage>, options?: Options)
-> AsyncGenerator<SDKMessage>
Critical Options:
outputFormat- Structured JSON schema validation (v0.1.45+)settingSources- Filesystem settings loading ('user'|'project'|'local')canUseTool- Custom permission logic callbackagents- Programmatic subagent definitionsmcpServers- MCP server configurationpermissionMode- 'default'|'acceptEdits'|'bypassPermissions'|'plan'betas- Enable beta features (e.g., 1M context window)sandbox- Sandbox settings for secure executionenableFileCheckpointing- Enable file state snapshotssystemPrompt- System prompt (string or preset object)
Extended Context (1M Tokens)
Enable 1 million token context window:
const response = query({
prompt: "Analyze this large codebase",
options: {
betas: ['context-1m-2025-08-07'], // Enable 1M context
model: "claude-sonnet-4-5"
}
});
System Prompt Configuration
Two forms of systemPrompt:
// 1. Simple string
systemPrompt: "You are a helpful coding assistant."
// 2. Preset with optional append (preserves Claude Code defaults)
systemPrompt: {
type: 'preset',
preset: 'claude_code',
append: "\n\nAdditional context: Focus on security."
}
Use preset form when you want Claude Code's default behaviors plus custom additions.
Tool Integration (Built-in + Custom)
Tool Control:
allowedTools- Whitelist (takes precedence)disallowedTools- BlacklistcanUseTool- Custom permission callback (see Permission Control section)
Built-in Tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, Task, NotebookEdit, BashOutput, KillBash, ListMcpResources, ReadMcpResource, AskUserQuestion
AskUserQuestion Tool (v0.1.71+)
Enable user interaction during agent execution:
const response = query({
prompt: "Review and refactor the codebase",
options: {
allowedTools: ["Read", "Write", "Edit", "AskUserQuestion"]
}
});
// Agent can now ask clarifying questions
// Questions appear in message stream as tool_call with name "AskUserQuestion"
Use cases:
- Clarify ambiguous requirements mid-task
- Get user approval before destructive operations
- Present options and get selection
Tools Configuration (v0.1.57+)
Three forms of tool configuration:
// 1. Exact allowlist (string array)
tools: ["Read", "Write", "Grep"]
// 2. Disable all tools (empty array)
tools: []
// 3. Preset with defaults (object form)
tools: { type: 'preset', preset: 'claude_code' }
Note: allowedTools and disallowedTools still work but tools provides more flexibility.
MCP Servers (Model Context Protocol)
Server Types:
- In-process -
createSdkMcpServer()withtool()definitions - External - stdio, HTTP, SSE transport
Tool Definition:
tool(name: string, description: string, zodSchema, handler)
Handler Return:
{ content: [{ type: "text", text: "..." }], isError?: boolean }
External MCP Servers (stdio)
const response = query({
prompt: "List files and analyze Git history",
options: {
mcpServers: {
// Filesystem server
"filesystem": {
command: "npx",
args: ["@modelcontextprotocol/server-filesystem"],
env: {
ALLOWED_PATHS: "/Users/developer/projects:/tmp"
}
},
// Git operations server
"git": {
command: "npx",
args: ["@modelcontextprotocol/server-git"],
env: {
GIT_REPO_PATH: "/Users/developer/projects/my-repo"
}
}
},
allowedTools: [
"mcp__filesystem__list_files",
"mcp__filesystem__read_file",
"mcp__git__log",
"mcp__git__diff"
]
}
});
External MCP Servers (HTTP/SSE)
const response = query({
prompt: "Analyze data from remote service",
options: {
mcpServers: {
"remote-service": {
url: "https://api.example.com/mcp",
headers: {
"Authorization": "Bearer your-token-here",
"Content-Type": "application/json"
}
}
},
allowedTools: ["mcp__remote-service__analyze"]
}
});
MCP Tool Naming Convention
Format: mcp__<server-name>__<tool-name>
CRITICAL:
- Server name and tool name MUST match configuration
- Use double underscores (
__) as separators - Include in
allowedToolsarray
Examples: mcp__weather-service__get_weather, mcp__filesystem__read_file
Subagent Orchestration
AgentDefinition Type
type AgentDefinition = {
description: string; // When to use this agent
prompt: string; // System prompt for agent
tools?: string[]; // Allowed tools (optional)
model?: 'sonnet' | 'opus' | 'haiku' | 'inherit'; // Model (optional)
skills?: string[]; // Skills to load (v0.2.10+)
maxTurns?: number; // Maximum turns before stopping (v0.2.10+)
}
Field Details:
- description: When to use agent (used by main agent for delegation)
- prompt: System prompt (defines role, inherits main context)
- tools: Allowed tools (if omitted, inherits from main agent)
- model: Model override (
haiku/sonnet/opus/inherit) - skills: Skills to load for agent (v0.2.10+)
- maxTurns: Limit agent to N turns before returning control (v0.2.10+)
Usage:
agents: {
"security-checker": {
description: "Security audits and vulnerability scanning",
prompt: "You check security. Scan for secrets, verify OWASP compliance.",
tools: ["Read", "Grep", "Bash"],
model: "sonnet",
skills: ["security-best-practices"], // Load specific skills
maxTurns: 10 // Limit to 10 turns
}
}
ā ļø Subagent Cleanup Warning
Known Issue: Subagents don't stop when parent agent stops (Issue #132)
When a parent agent is stopped (via cancellation or error), spawned subagents continue running as orphaned processes. This can lead to:
- Resource leaks
- Continued tool execution after parent stopped
- RAM out-of-memory in recursive scenarios (Claude Code Issue #4850)
Workaround: Implement cleanup in Stop hooks:
const response = query({
prompt: "Deploy to production",
options: {
agents: {
"deployer": {
description: "Handle deployments",
prompt: "Deploy the application",
tools: ["Bash"]
}
},
hooks: {
Stop: async (input) => {
// Manual cleanup of spawned processes
console.log("Parent stopped - cleaning up subagents");
// Implement process tracking and termination
}
}
}
});
Enhancement Tracking: Issue #142 proposes auto-termination
Session Management
Options:
resume: sessionId- Continue previous sessionforkSession: true- Create new branch from sessioncontinue: prompt- Resume with new prompt (differs fromresume)
Session Forking Pattern (Unique Capability):
// Explore alternative without modifying original
const forked = query({
prompt: "Try GraphQL instead of REST",
options: {
resume: sessionId,
forkSession: true // Creates new branch, original session unchanged
}
});
Capture Session ID:
for await (const message of response) {
if (message.type === 'system' && message.subtype === 'init') {
sessionId = message.session_id; // Save for later resume/fork
}
}
V2 Session APIs (Preview - v0.1.54+)
Simpler multi-turn conversation pattern:
import {
unstable_v2_createSession,
unstable_v2_resumeSession,
unstable_v2_prompt
} from "@anthropic-ai/claude-agent-sdk";
// Create a new session
const session = await unstable_v2_createSession({
model: "claude-sonnet-4-5",
workingDirectory: process.cwd(),
allowedTools: ["Read", "Grep", "Glob"]
});
// Send prompts and stream responses
const stream = unstable_v2_prompt(session, "Analyze the codebase structure");
for await (const message of stream) {
console.log(message);
}
// Continue conversation in same session
const stream2 = unstable_v2_prompt(session, "Now suggest improvements");
for await (const message of stream2) {
console.log(message);
}
// Resume a previous session
const resumedSession = await unstable_v2_resumeSession(session.sessionId);
Note: V2 APIs are in preview (unstable_ prefix). The .receive() method was renamed to .stream() in v0.1.72.
Permission Control
Permission Modes:
type PermissionMode = "default" | "acceptEdits" | "bypassPermissions" | "plan";
default- Standard permission checksacceptEdits- Auto-approve file editsbypassPermissions- Skip ALL checks (use in CI/CD only)plan- Planning mode (v0.1.45+)
Custom Permission Logic
const response = query({
prompt: "Deploy application to production",
options: {
permissionMode: "default",
canUseTool: async (toolName, input) => {
// Allow read-only operations
if (['Read', 'Grep', 'Glob'].includes(toolName)) {
return { behavior: "allow" };
}
// Deny destructive bash commands
if (toolName === 'Bash') {
const dangerous = ['rm -rf', 'dd if=', 'mkfs', '> /dev/'];
if (dangerous.some(pattern => input.command.includes(pattern))) {
return {
behavior: "deny",
message: "Destructive command blocked for safety"
};
}
}
// Require confirmation for deployments
if (input.command?.includes('deploy') || input.command?.includes('kubectl apply')) {
return {
behavior: "ask",
message: "Confirm deployment to production?"
};
}
// Allow by default
return { behavior: "allow" };
}
}
});
canUseTool Callback
type CanUseToolCallback = (
toolName: string,
input: any
) => Promise<PermissionDecision>;
type PermissionDecision =
| { behavior: "allow" }
| { behavior: "deny"; message?: string }
| { behavior: "ask"; message?: string };
Examples:
// Block all file writes
canUseTool: async (toolName, input) => {
if (toolName === 'Write' || toolName === 'Edit') {
return { behavior: "deny", message: "No file modifications allowed" };
}
return { behavior: "allow" };
}
// Require confirmation for specific files
canUseTool: async (toolName, input) => {
const sensitivePaths = ['/etc/', '/root/', '.env', 'credentials.json'];
if ((toolName === 'Write' || toolName === 'Edit') &&
sensitivePaths.some(path => input.file_path?.includes(path))) {
return {
behavior: "ask",
message: `Modify sensitive file ${input.file_path}?`
};
}
return { behavior: "allow" };
}
// Log all tool usage
canUseTool: async (toolName, input) => {
console.log(`Tool requested: ${toolName}`, input);
await logToDatabase(toolName, input);
return { behavior: "allow" };
}
Sandbox Settings (Security-Critical)
Enable sandboxed execution for Bash commands:
const response = query({
prompt: "Run system diagnostics",
options: {
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true, // Auto-approve bash in sandbox
excludedCommands: ["rm", "dd", "mkfs"], // Never auto-approve these
allowUnsandboxedCommands: false // Deny unsandboxable commands
}
}
});
SandboxSettings Type
type SandboxSettings = {
enabled: boolean;
autoAllowBashIfSandboxed?: boolean; // Default: false
excludedCommands?: string[];
allowUnsandboxedCommands?: boolean; // Default: false
network?: NetworkSandboxSettings;
ignoreViolations?: SandboxIgnoreViolations;
};
type NetworkSandboxSettings = {
enabled: boolean;
proxyUrl?: string; // HTTP proxy for network requests
};
Key Options:
enabled- Activate sandbox isolationautoAllowBashIfSandboxed- Skip permission prompts for safe bash commandsexcludedCommands- Commands that always require permissionallowUnsandboxedCommands- Allow commands that can't be sandboxed (risky)network.proxyUrl- Route network through proxy for monitoring
Best Practice: Always use sandbox in production agents handling untrusted input.
File Checkpointing
Enable file state snapshots for rollback capability:
const response = query({
prompt: "Refactor the authentication module",
options: {
enableFileCheckpointing: true // Enable file snapshots
}
});
// Later: rewind file changes to a specific point
for await (const message of response) {
if (message.type === 'user' && message.uuid) {
// Can rewind to this point later
const userMessageUuid = message.uuid;
// To rewind (call on Query object)
await response.rewindFiles(userMessageUuid);
}
}
Use cases:
- Undo failed refactoring attempts
- A/B test code changes
- Safe exploration of alternatives
Filesystem Settings
Setting Sources:
type SettingSource = 'user' | 'project' | 'local';
user-~/.claude/settings.json(global)project-.claude/settings.json(team-shared)local-.claude/settings.local.json(gitignored overrides)
Default: NO settings loaded (settingSources: [])
Settings Priority
When multiple sources loaded, settings merge in this order (highest priority first):
- Programmatic options (passed to
query()) - Always win - Local settings (
.claude/settings.local.json) - Project settings (
.claude/settings.json) - User settings (
~/.claude/settings.json)
Example:
// .claude/settings.json
{
"allowedTools": ["Read", "Write", "Edit"]
}
// .claude/settings.local.json
{
"allowedTools": ["Read"] // Overrides project settings
}
// Programmatic
const response = query({
options: {
settingSources: ["project", "local"],
allowedTools: ["Read", "Grep"] // ā This wins
}
});
// Actual allowedTools: ["Read", "Grep"]
Best Practice: Use settingSources: ["project"] in CI/CD for consistent behavior.
Query Object Methods
The query() function returns a Query object with these methods:
const q = query({ prompt: "..." });
// Async iteration (primary usage)
for await (const message of q) { ... }
// Runtime model control
await q.setModel("claude-opus-4-5"); // Change model mid-session
await q.setMaxThinkingTokens(4096); // Set thinking budget
// Introspection
const models = await q.supportedModels(); // List available models
const commands = await q.supportedCommands(); // List available commands
const account = await q.accountInfo(); // Get account details
// MCP status
const status = await q.mcpServerStatus(); // Check MCP server status
// Returns: { [serverName]: { status: 'connected' | 'failed', error?: string } }
// File operations (requires enableFileCheckpointing)
await q.rewindFiles(userMessageUuid); // Rewind to checkpoint
Use cases:
- Dynamic model switching based on task complexity
- Monitoring MCP server health
- Adjusting thinking budget for reasoning tasks
Message Types & Streaming
Message Types:
system- Session init/completion (includessession_id)assistant- Agent responsestool_call- Tool execution requeststool_result- Tool execution resultserror- Error messagesresult- Final result (includesstructured_outputfor v0.1.45+)
Streaming Pattern:
for await (const message of response) {
if (message.type === 'system' && message.subtype === 'init') {
sessionId = message.session_id; // Capture for resume/fork
}
if (message.type === 'result' && message.structured_output) {
// Structured output available (v0.1.45+)
const validated = schema.parse(message.structured_output);
}
}
Error Handling
Error Codes:
| Error Code | Cause | Solution |
|---|---|---|
CLI_NOT_FOUND |
Claude Code not installed | Install: npm install -g @anthropic-ai/claude-code |
AUTHENTICATION_FAILED |
Invalid API key | Check ANTHROPIC_API_KEY env var |
RATE_LIMIT_EXCEEDED |
Too many requests | Implement retry with backoff |
CONTEXT_LENGTH_EXCEEDED |
Prompt too long | Use session compaction, reduce context |
PERMISSION_DENIED |
Tool blocked | Check permissionMode, canUseTool |
TOOL_EXECUTION_FAILED |
Tool error | Check tool implementation |
SESSION_NOT_FOUND |
Invalid session ID | Verify session ID |
MCP_SERVER_FAILED |
Server error | Check server configuration |
Known Issues Prevention
This skill prevents 14 documented issues:
Issue #1: CLI Not Found Error
Error: "Claude Code CLI not installed"
Source: SDK requires Claude Code CLI
Why It Happens: CLI not installed globally
Prevention: Install before using SDK: npm install -g @anthropic-ai/claude-code
Issue #2: Authentication Failed
Error: "Invalid API key"
Source: Missing or incorrect ANTHROPIC_API_KEY
Why It Happens: Environment variable not set
Prevention: Always set export ANTHROPIC_API_KEY="sk-ant-..."
Issue #3: Permission Denied Errors
Error: Tool execution blocked
Source: permissionMode restrictions
Why It Happens: Tool not allowed by permissions
Prevention: Use allowedTools or custom canUseTool callback
Issue #4: Context Length Exceeded (Session-Breaking)
Error: "Prompt too long"
Source: Input exceeds model context window (Issue #138)
Why It Happens: Large codebase, long conversations
ā ļø Critical Behavior: Once a session hits context limit:
- All subsequent requests to that session return "Prompt too long"
/compactcommand fails with same error- Session is permanently broken and must be abandoned
Prevention Strategies:
// 1. Proactive session forking (create checkpoints before hitting limit)
const checkpoint = query({
prompt: "Checkpoint current state",
options: {
resume: sessionId,
forkSession: true // Create branch before hitting limit
}
});
// 2. Monitor time and rotate sessions proactively
const MAX_SESSION_TIME = 80 * 60 * 1000; // 80 minutes (before 90-min crash)
let sessionStartTime = Date.now();
function shouldRotateSession() {
return Date.now() - sessionStartTime > MAX_SESSION_TIME;
}
// 3. Start new sessions before hitting context limits
if (shouldRotateSession()) {
const summary = await getSummary(currentSession);
const newSession = query({
prompt: `Continue with context: ${summary}`
});
sessionStartTime = Date.now();
}
Note: SDK auto-compacts, but if limit is reached, session becomes unrecoverable
Issue #5: Tool Execution Timeout
Error: Tool doesn't respond Source: Long-running tool execution Why It Happens: Tool takes too long (>5 minutes default) Prevention: Implement timeout handling in tool implementations
Issue #6: Session Not Found
Error: "Invalid session ID"
Source: Session expired or invalid
Why It Happens: Session ID incorrect or too old
Prevention: Capture session_id from system init message
Issue #7: MCP Server Connection Failed
Error: Server not responding Source: Server not running or misconfigured Why It Happens: Command/URL incorrect, server crashed Prevention: Test MCP server independently, verify command/URL
Issue #8: Subagent Definition Errors
Error: Invalid AgentDefinition
Source: Missing required fields
Why It Happens: description or prompt missing
Prevention: Always include description and prompt fields
Issue #9: Settings File Not Found
Error: "Cannot read settings"
Source: Settings file doesn't exist
Why It Happens: settingSources includes non-existent file
Prevention: Check file exists before including in sources
Issue #10: Tool Name Collision
Error: Duplicate tool name Source: Multiple tools with same name Why It Happens: Two MCP servers define same tool name Prevention: Use unique tool names, prefix with server name
Issue #11: Zod Schema Validation Error
Error: Invalid tool input
Source: Input doesn't match Zod schema
Why It Happens: Agent provided wrong data type
Prevention: Use descriptive Zod schemas with .describe()
Issue #12: Filesystem Permission Denied
Error: Cannot access path
Source: Restricted filesystem access
Why It Happens: Path outside workingDirectory or no permissions
Prevention: Set correct workingDirectory, check file permissions
Issue #13: MCP Server Config Missing type Field
Error: "Claude Code process exited with code 1" (cryptic, no context)
Source: GitHub Issue #131
Why It Happens: URL-based MCP servers require explicit type: "http" or type: "sse" field
Prevention: Always specify transport type for URL-based MCP servers
// ā Wrong - missing type field (causes cryptic exit code 1)
mcpServers: {
"my-server": {
url: "https://api.example.com/mcp"
}
}
// ā
Correct - type field required for URL-based servers
mcpServers: {
"my-server": {
url: "https://api.example.com/mcp",
type: "http" // or "sse" for Server-Sent Events
}
}
Diagnostic Clue: If you see "process exited with code 1" with no other context, check your MCP server configuration for missing type fields.
Issue #14: MCP Tool Result with Unicode Line Separators
Error: JSON parse error, agent hangs Source: GitHub Issue #137 Why It Happens: Unicode U+2028 (line separator) and U+2029 (paragraph separator) are valid in JSON but break JavaScript parsing Prevention: Escape these characters in MCP tool results
// MCP tool handler - sanitize external data
tool("fetch_content", "Fetch text content", {}, async (args) => {
const content = await fetchExternalData();
// ā
Sanitize Unicode line/paragraph separators
const sanitized = content
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
return {
content: [{ type: "text", text: sanitized }]
};
});
When This Matters: External data sources (APIs, web scraping, user input) that may contain these characters
Related: MCP Python SDK Issue #1356
Official Documentation
- Agent SDK Overview: https://platform.claude.com/docs/en/api/agent-sdk/overview
- TypeScript API: https://platform.claude.com/docs/en/api/agent-sdk/typescript
- Structured Outputs: https://platform.claude.com/docs/en/agent-sdk/structured-outputs
- GitHub (TypeScript): https://github.com/anthropics/claude-agent-sdk-typescript
- CHANGELOG: https://github.com/anthropics/claude-agent-sdk-typescript/blob/main/CHANGELOG.md
Token Efficiency:
- Without skill: ~15,000 tokens (MCP setup, permission patterns, session APIs, sandbox config, hooks, structured outputs, error handling)
- With skill: ~4,500 tokens (comprehensive v0.2.12 coverage + error prevention + advanced patterns)
- Savings: ~70% (~10,500 tokens)
Errors prevented: 14 documented issues with exact solutions (including 2 community-sourced gotchas) Key value: V2 Session APIs, Sandbox Settings, File Checkpointing, Query methods, AskUserQuestion tool, structured outputs (v0.1.45+), session forking, canUseTool patterns, complete hooks system (12 events), Zod v4 support, subagent cleanup patterns
Last verified: 2026-01-20 | Skill version: 3.1.0 | Changes: Added Issue #13 (MCP type field), Issue #14 (Unicode U+2028/U+2029), expanded Issue #4 (session-breaking), added subagent cleanup warning with Stop hook pattern