Claude Agent SDK: permissions, custom tools and subagents
The SDK ships the tools and the loop; what you author is Options. How allowedTools, tools and disallowedTools differ, why delegation needs the Agent tool, and where custom tools, hooks and skills go.
You are configuring a harness
The Claude Agent SDK is Claude Code packaged as a library. You call query({ prompt, options }) and the SDK runs the same agent loop, context management and built-in tools — Read, Write, Edit, Bash, Glob, Grep, web search and fetch — that power Claude Code, in your own process. Subagents, hooks, sessions, skills and MCP come with it.
That changes what "building the agent" means. There is no tool loop to write and no filesystem layer to invent. What you author is the Options object:
import { query } from "@anthropic-ai/claude-agent-sdk"
for await (const message of query({
prompt: "Review the authentication module for security issues",
options: {
systemPrompt,
allowedTools: ["Read", "Grep", "Glob", "Agent"],
agents: { "code-reviewer": codeReviewer },
},
})) {
if (message.type === "result") console.log(message.result)
}
That object is where every decision meets, which is why the free Claude Agent SDK generator opens its preview on src/options.ts. The rest of the generated tree — one file per custom tool, subagent and hook — exists so each piece can be edited alone.
It is a different product from two things it gets confused with. The Anthropic API client SDK is where you write the tool loop yourself. Managed Agents is where Anthropic hosts the loop and a sandbox. The Agent SDK is the harness, on your infrastructure.
The system prompt: yours, or Claude Code's plus yours
systemPrompt takes a string, or { type: "preset", preset: "claude_code", append: "…" }. The preset is Claude Code's own system prompt — how to use the built-in tools, the conventions it follows — with your text appended. A bare string sends only your text; the model still has the tools, but not the harness's instructions for them. Either is defensible. A custom prompt that is empty is not: the SDK adds nothing of its own, so the model has tools and no role.
Three permission layers that look like one
tools, allowedTools and disallowedTools do different jobs, and the difference is the most common source of "why did it do that".
toolsis availability.tools: ["Read", "Grep"]means only those built-ins exist in the model's context; everything else is absent, not merely un-approved. MCP tools are unaffected.allowedToolsis permission. Listed tools run without a prompt. Unlisted tools still exist and fall through to the permission mode, then to yourcanUseToolcallback.disallowedToolsis both. A bare name likeBashremoves the tool from context. A scoped rule likeBash(rm *)leaves the tool visible and denies matching calls — in every mode, includingbypassPermissions.
Evaluation order is hooks → deny rules → ask rules → permission mode → allow rules → canUseTool. Two consequences the docs stress and the generator enforces:
allowedToolsdoes not constrainbypassPermissions. In that mode every tool is approved, not just the ones you listed. If something must stay blocked, it goes in the deny rules.- A tool approved by an allow rule never reaches
canUseTool. A check that must run on every call belongs in aPreToolUsehook, which runs before everything else and can deny even under bypass.
A name in both the allow list and the deny rules is a dead allow — deny is checked first — and the generator says so next to the field.
The permission modes: default, acceptEdits and plan send un-approved calls to canUseTool; dontAsk denies them outright and never calls it; auto lets a model classifier decide; bypassPermissions approves everything. For a headless agent with a fixed tool surface, allowedTools plus dontAsk is the locked-down shape.
Custom tools are an in-process MCP server
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk"
import { z } from "zod"
const lookupOrder = tool(
"lookup_order",
"Look up one order by its id and return its status and delivery estimate.",
{ orderId: z.string().describe("The order id, as printed on the confirmation email.") },
async (args) => ({ content: [{ type: "text", text: JSON.stringify(await orders.get(args.orderId)) }] }),
{ annotations: { readOnlyHint: true } }
)
export const server = createSdkMcpServer({ name: "app", version: "1.0.0", tools: [lookupOrder] })
The server runs inside your process — no child process, no socket — and goes in mcpServers under a key. Each tool then reaches the model as mcp__<key>__<name>: here, mcp__app__lookup_order. That string is what allowedTools matches, so both the key and the tool name are identifiers, and the generator checks rather than repairs them. readOnlyHint: true lets the harness run the tool in parallel with other read-only calls; it is metadata, not enforcement, so keep it honest.
A handler that throws still reaches Claude as an error result. Returning { isError: true, content: [...] } yourself lets you compose the message Claude reads, which is usually better than the raw exception.
Subagents need the Agent tool
A subagent is a key in agents:
const codeReviewer: AgentDefinition = {
description: "Expert code review specialist. Use for security and maintainability reviews.",
prompt: "You are a code review specialist…",
tools: ["Read", "Grep", "Glob"],
model: "sonnet",
effort: "high",
}
description and prompt are required: the first is what Claude reads to decide whether to delegate, the second is the subagent's whole role, because it never sees the parent's system prompt or conversation — only its own prompt and the task text the parent writes. tools restricts; omit it to inherit every tool available to subagents. model takes an alias or a full id, effort a level, background: true forces a non-blocking run.
The part that silently fails: Claude reaches a subagent by calling the built-in Agent tool. If Agent is not in allowedTools, every delegation falls through to the permission prompt — or is denied outright in dontAsk — and Claude does the work itself. The generator refuses to let subagents exist without Agent approved unless the mode is bypass.
Subagents spawn subagents of their own, and Claude Opus 5 delegates readily. maxBudgetUsd on the query, compared against total_cost_usd including every subagent's requests, is the cap; CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH and CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS in env bound the tree.
Hooks, skills and structured output
Hooks are callbacks keyed by event in options.hooks. PreToolUse runs before a call and can deny it:
const protectEnv: HookCallback = async (input) => {
const pre = input as PreToolUseHookInput
return {
hookSpecificOutput: {
hookEventName: pre.hook_event_name,
permissionDecision: "deny",
permissionDecisionReason: "Cannot modify .env files",
},
}
}
hooks: { PreToolUse: [{ matcher: "Write|Edit", hooks: [protectEnv] }] }
Every other event — PostToolUse, Stop, SessionStart, SubagentStop and the rest — observes. Only PreToolUse can deny, and a matcher only means something on the four events that carry a tool name.
Skills are files, not options: .claude/skills/<name>/SKILL.md with name and description frontmatter, discovered when settingSources includes "project", and allowed by name through skills: [...]. Claude invokes one when a request matches its description; you can dispatch one directly with /<name> in the prompt. One trap: an explicit tools list that omits Skill removes the Skill tool from context, and no skill can be invoked.
Structured output is outputFormat: { type: "json_schema", schema }. The SDK validates against JSON Schema draft-07, and Zod targets 2020-12 by default, so the conversion is z.toJSONSchema(OutputSchema, { target: "draft-7" }). The result arrives as message.structured_output on a success result.
Where to start
npm install @anthropic-ai/claude-agent-sdk zod
Set ANTHROPIC_API_KEY — the docs are explicit that third-party agents use API keys, not claude.ai logins — write a system prompt, and call query. If you would rather see the whole Options object first, with the custom tools on their server, the subagent definitions, a deny hook and a skill, the Claude Agent SDK generator produces the project as a downloadable .zip, free and without an account, and reports each of the permission traps above next to the field that causes it.
The same form exists for the OpenAI Agents SDK, LangChain Deep Agents and Google ADK, and the MCP server generator builds the external servers any of them can mount.
Target-specific scaffolding
Continue this learning path
Permissions, manifests, capabilities, agent projects and MCP servers for browser extensions, desktop, mobile, and backend agents.
Design your application
Design Web, Desktop, Mobile, Terminal, or Extension projects with a target-aware canvas and exporter.
Open projects