| name | claude-agent-sdk-best-practice |
| description | Claude Agent SDK v0.2.101 best practices and complete API reference. Use when: building AI agents with @anthropic-ai/claude-agent-sdk, designing multi-agent systems, configuring MCP servers, implementing hooks/permissions, using structured outputs, managing sessions, or troubleshooting SDK errors. Covers query() API, V2 session API, 22 hook events, sandbox, worktree isolation, and current public configuration options.
|
| allowed-tools | Read,Glob,Grep,Bash,Write,Edit,Task |
| version | 1.2.0 |
Claude Agent SDK Best Practice Guide
Package: @anthropic-ai/claude-agent-sdk@0.2.101 | Peer dep: zod ^4.0.0 | Node.js: >= 18 | ESM only
Quick Reference
Core Concepts
| Concept | Description | Key API |
|---|
query() | Primary V1 API - returns async generator | query({ prompt, options }) |
| V2 Session | Multi-turn persistent session (unstable) | unstable_v2_createSession() |
| Agents | Subagent definitions for task delegation | options.agents |
| MCP Servers | Custom tools via Model Context Protocol | createSdkMcpServer(), tool() |
| Hooks | 22 event callbacks for middleware | options.hooks |
| Structured Output | JSON schema validated responses | options.outputFormat |
| Permissions | Fine-grained tool access control | canUseTool, permissionMode |
| Sandbox | Secure execution isolation | options.sandbox |
Essential Imports
import {
forkSession,
getSessionInfo,
getSessionMessages,
listSessions,
listSubagents,
getSubagentMessages,
query,
renameSession,
tagSession,
createSdkMcpServer,
tool,
unstable_v2_createSession,
unstable_v2_resumeSession,
unstable_v2_prompt,
} from "@anthropic-ai/claude-agent-sdk";
Key Configuration Patterns
const q = query({ prompt: "Analyze this code", options: { model: "claude-sonnet-4-6" } });
const q = query({
prompt: "Deploy to staging",
options: {
model: "claude-sonnet-4-6",
permissionMode: "default",
allowedTools: ["Read", "Grep", "Glob", "Bash"],
sandbox: { enabled: true, autoAllowBashIfSandboxed: true },
maxTurns: 20,
maxBudgetUsd: 5.0,
thinking: { type: "adaptive" },
}
});
const q = query({
prompt: taskFromCI,
options: {
model: "claude-sonnet-4-6",
permissionMode: "bypassPermissions",
settingSources: ["project"],
sandbox: { enabled: true },
maxBudgetUsd: 10.0,
taskBudget: 50,
includeHookEvents: true,
outputFormat: { type: "json_schema", schema: resultSchema },
}
});
Query API (V1) - Core Pattern
const q = query({ prompt: "...", options: { ... } });
for await (const message of q) {
switch (message.type) {
case "system":
if (message.subtype === "init") sessionId = message.session_id;
break;
case "assistant":
break;
case "result":
if (message.subtype === "success") {
console.log(message.result);
console.log(message.structured_output);
console.log(`Cost: $${message.total_cost_usd}`);
}
break;
}
}
Query Control Methods
const q = query({ prompt: "..." });
await q.interrupt();
await q.setModel("claude-opus-4-6");
await q.setPermissionMode("acceptEdits");
q.close();
const agents = await q.supportedAgents();
const models = await q.supportedModels();
const status = await q.mcpServerStatus();
const account = await q.accountInfo();
const usage = await q.getContextUsage();
const settings = await q.getSettings();
const subagents = await q.listSubagents(sessionId);
const subMsgs = await q.getSubagentMessages(sessionId, subagentId);
await q.reconnectMcpServer("my-server");
await q.toggleMcpServer("my-server", false);
await q.setMcpServers({ "new-server": config });
await q.enableChannel("my-server", "channel-name");
await q.reloadPlugins();
await q.rewindFiles(userMessageId);
Structured Output
const q = query({
prompt: "Analyze this PR",
options: {
outputFormat: {
type: "json_schema",
schema: {
type: "object",
properties: {
summary: { type: "string" },
risk_level: { type: "string", enum: ["low", "medium", "high"] },
issues: { type: "array", items: { type: "string" } }
},
required: ["summary", "risk_level", "issues"]
}
}
}
});
for await (const msg of q) {
if (msg.type === "result" && msg.subtype === "success") {
const output = msg.structured_output;
}
}
Error: If validation fails repeatedly → subtype: "error_max_structured_output_retries"
Agent Definitions (Subagents)
options: {
agents: {
"code-reviewer": {
description: "Review code for bugs and style issues",
prompt: "You are a code reviewer. Check for bugs, security issues, and style.",
tools: ["Read", "Grep", "Glob"],
model: "sonnet",
maxTurns: 15,
},
"test-runner": {
description: "Run test suites and report results",
prompt: "Run all tests. Report failures clearly with file:line references.",
tools: ["Bash", "Read"],
model: "haiku",
maxTurns: 10,
},
"architect": {
description: "Design system architecture for complex features",
prompt: "You design software architecture. Consider scalability and maintainability.",
model: "opus",
skills: ["system-design-patterns"],
}
}
}
AgentDefinition fields: description (required), prompt (required), tools?, disallowedTools?, model? ('sonnet'|'opus'|'haiku'|'inherit'), mcpServers?, skills?, maxTurns?, criticalSystemReminder_EXPERIMENTAL?
Runtime spawn (Agent tool): supports run_in_background, isolation: "worktree" (git worktree), mode, resume (agent ID)
MCP Servers
In-Process Server (Recommended)
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
const server = createSdkMcpServer({
name: "my-tools",
version: "1.0.0",
tools: [
tool(
"search_tickets",
"Search support tickets by keyword",
{
query: z.string().describe("Search keyword"),
status: z.enum(["open", "closed", "all"]).describe("Filter by status"),
},
async (args) => {
const results = await db.searchTickets(args.query, args.status);
return { content: [{ type: "text", text: JSON.stringify(results) }] };
},
{ annotations: { readOnly: true } }
)
]
});
query({ prompt: "...", options: { mcpServers: { "my-tools": server } } });
External Servers
mcpServers: {
"fs": { command: "npx", args: ["@modelcontextprotocol/server-filesystem"] },
"api": { type: "http", url: "https://api.example.com/mcp", headers: { Authorization: "Bearer ..." } },
"stream": { type: "sse", url: "https://stream.example.com/mcp" },
}
Tool naming: mcp__<server-name>__<tool-name> (double underscores)
Hooks (22 Events)
hooks: {
PreToolUse: [{ matcher: "Bash", hooks: [async (input) => {
const command = (input.tool_input as { command?: string }).command ?? "";
if (command.includes("rm -rf")) {
return { decision: "block", reason: "Destructive command blocked" };
}
return { decision: "approve" };
}] }],
PostToolUse: [{ hooks: [async (input) => {
await auditLog(input.tool_name, input.tool_input, input.tool_response);
return {};
}] }],
SessionStart: [{ hooks: [async (input) => {
return { systemMessage: "Additional context for this session" };
}] }],
Stop: [{ hooks: [async (input) => {
await cleanup();
return {};
}] }],
}
All 22 events: PreToolUse, PostToolUse, PostToolUseFailure, Notification, UserPromptSubmit, SessionStart, SessionEnd, Stop, SubagentStart, SubagentStop, PreCompact, PostCompact, PermissionRequest, Setup, TeammateIdle, TaskCompleted, Elicitation, ElicitationResult, ConfigChange, WorktreeCreate, WorktreeRemove, InstructionsLoaded
Permission System
canUseTool: async (toolName, input, options) => {
if (["Read", "Grep", "Glob"].includes(toolName))
return { behavior: "allow" };
if (toolName === "Bash" && /rm\s+-rf|dd\s+if=/.test(input.command))
return { behavior: "deny", message: "Destructive command blocked" };
return { behavior: "allow", updatedInput: input };
}
V2 Session API (Unstable)
const session = unstable_v2_createSession({
model: "claude-sonnet-4-6",
permissionMode: "acceptEdits",
allowedTools: ["Read", "Write", "Edit", "Bash"],
});
await session.send("Build a REST API for user management");
for await (const msg of session.stream()) {
console.log(msg);
}
await session.send("Now add authentication");
for await (const msg of session.stream()) { ... }
session.close();
const resumed = unstable_v2_resumeSession(session.sessionId, { ... });
Anti-Patterns & Common Mistakes
1. Missing type on URL-based MCP servers
mcpServers: { "api": { url: "https://..." } }
mcpServers: { "api": { type: "http", url: "https://..." } }
2. Unicode line separators in MCP tool results
const sanitized = content.replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
3. Using bypassPermissions in production
permissionMode: "bypassPermissions"
permissionMode: "default",
sandbox: { enabled: true, autoAllowBashIfSandboxed: true },
canUseTool: async (tool, input) => { }
4. Not capturing session ID
for await (const msg of q) {
if (msg.type === "system" && msg.subtype === "init") {
sessionId = msg.session_id;
}
}
5. Ignoring maxTurns and maxBudgetUsd
options: { maxTurns: 30, maxBudgetUsd: 10.0 }
6. Sandbox failing silently when unavailable
sandbox: { enabled: true }
sandbox: { enabled: true, failIfUnavailable: false }
sandbox: { enabled: true, failIfUnavailable: true }
7. Orphaned subagents on parent stop
hooks: { Stop: [{ hooks: [async () => { await cleanupSubagents(); }] }] }
Code Review Checklist
Reference Docs
Last verified: 2026-04-11 | SDK version: 0.2.101 | Claude Code parity: v2.1.101