Loading...
Loading...
Build autonomous AI agents with Claude Agent SDK. Structured outputs guarantee JSON schema validation, with plugins system and hooks for event-driven workflows. Prevents 14 documented errors. Use when: building coding agents, SRE systems, security auditors, or troubleshooting CLI not found, structured output validation, session forking errors, MCP config issues, subagent cleanup.
npx skill4agent add jezweb/claude-skills claude-agent-sdkoutputFormatmessage.structured_outputstructured-outputs-2025-11-13import { 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}`);
}
}import { z } from "zod"plugins| Hook | When Fired | Use Case |
|---|---|---|
| Before tool execution | Validate, modify, or block tool calls |
| After tool execution | Log results, trigger side effects |
| Agent notifications | Display status updates |
| User prompt received | Pre-process or validate input |
| Subagent spawned | Track delegation, log context |
| Subagent completed | Aggregate results, cleanup |
| Before context compaction | Save state before truncation |
| Permission needed | Custom approval workflows |
| Agent stopping | Cleanup, final logging |
| Session begins | Initialize state |
| Session ends | Persist state, cleanup |
| Error occurred | Custom error handling |
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);
}
}
}
});fallbackModelmaxThinkingTokensstrictMcpConfigcontinueresumepermissionMode: 'plan'query(prompt: string | AsyncIterable<SDKUserMessage>, options?: Options)
-> AsyncGenerator<SDKMessage>outputFormatsettingSourcescanUseToolagentsmcpServerspermissionModebetassandboxenableFileCheckpointingsystemPromptconst response = query({
prompt: "Analyze this large codebase",
options: {
betas: ['context-1m-2025-08-07'], // Enable 1M context
model: "claude-sonnet-4-5"
}
});// 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."
}allowedToolsdisallowedToolscanUseToolconst 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"// 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' }allowedToolsdisallowedToolstoolscreateSdkMcpServer()tool()tool(name: string, description: string, zodSchema, handler){ content: [{ type: "text", text: "..." }], isError?: boolean }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"
]
}
});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__<server-name>__<tool-name>__allowedToolsmcp__weather-service__get_weathermcp__filesystem__read_filetype 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+)
}haikusonnetopusinheritagents: {
"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
}
}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
}
}
}
});resume: sessionIdforkSession: truecontinue: promptresume// 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
}
});for await (const message of response) {
if (message.type === 'system' && message.subtype === 'init') {
sessionId = message.session_id; // Save for later resume/fork
}
}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);unstable_.receive().stream()type PermissionMode = "default" | "acceptEdits" | "bypassPermissions" | "plan";defaultacceptEditsbypassPermissionsplanconst 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" };
}
}
});type CanUseToolCallback = (
toolName: string,
input: any
) => Promise<PermissionDecision>;
type PermissionDecision =
| { behavior: "allow" }
| { behavior: "deny"; message?: string }
| { behavior: "ask"; message?: string };// 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" };
}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
}
}
});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
};enabledautoAllowBashIfSandboxedexcludedCommandsallowUnsandboxedCommandsnetwork.proxyUrlconst 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);
}
}type SettingSource = 'user' | 'project' | 'local';user~/.claude/settings.jsonproject.claude/settings.jsonlocal.claude/settings.local.jsonsettingSources: []query().claude/settings.local.json.claude/settings.json~/.claude/settings.json// .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"]settingSources: ["project"]query()Queryconst 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 checkpointsystemsession_idassistanttool_calltool_resulterrorresultstructured_outputfor 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 Code | Cause | Solution |
|---|---|---|
| Claude Code not installed | Install: |
| Invalid API key | Check ANTHROPIC_API_KEY env var |
| Too many requests | Implement retry with backoff |
| Prompt too long | Use session compaction, reduce context |
| Tool blocked | Check permissionMode, canUseTool |
| Tool error | Check tool implementation |
| Invalid session ID | Verify session ID |
| Server error | Check server configuration |
"Claude Code CLI not installed"npm install -g @anthropic-ai/claude-code"Invalid API key"export ANTHROPIC_API_KEY="sk-ant-..."permissionModeallowedToolscanUseTool"Prompt too long"/compact// 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();
}"Invalid session ID"session_idsystemdescriptionpromptdescriptionprompt"Cannot read settings"settingSources.describe()workingDirectoryworkingDirectorytype"Claude Code process exited with code 1"type: "http"type: "sse"// ❌ 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
}
}type// 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 }]
};
});