Agent SDK Builder skill for Claude Code

Ship a custom agent on Claude Code's own loop. Builds agents on the Claude Agent SDK that are safe to run inside a product. Claude starts with a read-only loop that logs every message type, adds domain tools as in-process MCP servers, gates risky calls with canUseTool or hooks, chooses the system prompt preset and settingSources on purpose so CLAUDE.md and skills load only when intended, and bounds every run by turns, time and logged cost.

  • nameagent-sdk-builder
  • size232 lines · ≈2,550 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Builds custom agents on the Claude Agent SDK in TypeScript or Python, covering the query loop and message types, allowed tools and permission modes, canUseTool callbacks, custom in-process MCP tools, system prompt presets, settingSources for CLAUDE.md and skills, sessions and resume, subagents, hooks, cost limits, and deployment. Use when the user wants to build an agent, bot, or automation on the Claude Agent SDK, embed Claude Code's agent loop in an app or service, or migrate from claude -p scripts or the older Claude Code SDK.

Try asking

  • Build a support agent with the Agent SDK that can look up and refund orders
  • Port our claude -p bash script to a Python Agent SDK service with sessions
  • Why doesn't my SDK agent see our CLAUDE.md or project skills?

Use it when

  • Embedding an agent with file, shell or custom tools in your own app
  • A claude -p script has outgrown shell and needs sessions or permission logic
  • Adding domain tools and per-call permission checks to an agent
  • Migrating from the older Claude Code SDK package

Skip it when

How to check the result

The happy path returns a success result, forbidden edits and commands are denied with your message, a looping task stops at maxTurns, and cost per run is logged.

The common mistake it prevents

Assuming the agent picks up CLAUDE.md, skills and project permissions automatically, when nothing from the filesystem loads until settingSources includes it.

~/.claude/skills/agent-sdk-builder/SKILL.md

--- name: agent-sdk-builder description: Builds custom agents on the Claude Agent SDK in TypeScript or Python, covering the query loop and message types, allowed tools and… license: MIT ---

An SDK agent is ready when its tool surface is the minimum the job needs, every dangerous action passes through an explicit permission decision, runs are bounded by turns and budget, results are read from the typed result message rather than scraped text, and the behavior is pinned (model, prompt, settings) so it is reproducible.

Pick the right layer

NeedUse
One-off or CI automation driven by a shell scriptclaude -p (see headless-ci-agent)
An agent inside your app/service with file, shell, and web tools, sessions, subagentsClaude Agent SDK
Plain model calls, your own tool loop, no filesystem agentMessages API (see llm-app-builder)

Install and auth

npm install @anthropic-ai/claude-agent-sdk     # TypeScript (Node 18+)
pip install claude-agent-sdk                   # Python 3.10+
export ANTHROPIC_API_KEY=...                   # or CLAUDE_CODE_USE_BEDROCK=1 / CLAUDE_CODE_USE_VERTEX=1 with cloud creds

The SDK runs the Claude Code agent runtime as a subprocess; check the package README for whether your version bundles it or needs the CLI installed.

Workflow

- [ ] 1. Write the job spec: inputs, outputs, allowed side effects, budget
- [ ] 2. Minimal loop with read-only tools; print every message type
- [ ] 3. Add tools: built-ins via allowedTools, domain tools as SDK MCP tools
- [ ] 4. Add permission control (permissionMode + canUseTool or hooks)
- [ ] 5. Set system prompt and settingSources deliberately
- [ ] 6. Add sessions, subagents, structured output as needed
- [ ] 7. Bound it (maxTurns, timeouts, cost tracking) and test failure paths

2. Minimal loop (TypeScript)

import { query } from "@anthropic-ai/claude-agent-sdk";

const run = query({
  prompt: "Find why `pnpm test --filter api` fails and summarize the root cause. Do not edit files.",
  options: {
    cwd: "/srv/checkouts/shop",
    allowedTools: ["Read", "Grep", "Glob", "Bash"],
    permissionMode: "default",
    maxTurns: 15,
    systemPrompt: { type: "preset", preset: "claude_code", append: "You run unattended. Never ask questions." },
    settingSources: ["project"],            // load the repo's CLAUDE.md and .claude/settings.json
  },
});

for await (const msg of run) {
  if (msg.type === "system" && msg.subtype === "init") console.error("session", msg.session_id);
  if (msg.type === "assistant") {
    for (const block of msg.message.content) if (block.type === "text") console.error(block.text);
  }
  if (msg.type === "result") {
    if (msg.subtype === "success") console.log(msg.result);
    else console.error("failed:", msg.subtype);          // e.g. error_max_turns, error_during_execution
    console.error(`turns=${msg.num_turns} cost=$${msg.total_cost_usd}`);
  }
}

Python equivalent:

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ResultMessage, TextBlock

async def main():
    options = ClaudeAgentOptions(
        cwd="/srv/checkouts/shop",
        allowed_tools=["Read", "Grep", "Glob", "Bash"],
        max_turns=15,
        system_prompt={"type": "preset", "preset": "claude_code", "append": "You run unattended."},
        setting_sources=["project"],
    )
    async for msg in query(prompt="Find why the api tests fail; do not edit files.", options=options):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if isinstance(block, TextBlock):
                    print(block.text)
        elif isinstance(msg, ResultMessage):
            print(msg.subtype, msg.num_turns, msg.total_cost_usd)

asyncio.run(main())

For multi-turn conversations in Python, use ClaudeSDKClient (async with ClaudeSDKClient(options) as client: await client.query(...); async for m in client.receive_response(): ...).

3. Custom tools (in-process MCP)

import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";

const lookupOrder = tool(
  "lookup_order",
  "Look up an order by ID (format ord_...). Returns status, total_cents, items. Use before answering any order question.",
  { orderId: z.string().regex(/^ord_/) },
  async ({ orderId }) => {
    const order = await orders.find(orderId);
    if (!order) return { isError: true, content: [{ type: "text", text: `No order ${orderId}. Ask for the ID from the confirmation email.` }] };
    return { content: [{ type: "text", text: JSON.stringify(order) }] };
  },
);
const shop = createSdkMcpServer({ name: "shop", version: "1.0.0", tools: [lookupOrder] });

async function* input() {
  yield { type: "user" as const, message: { role: "user" as const, content: "Where is order ord_8812?" } };
}
for await (const msg of query({
  prompt: input(),                                   // SDK MCP tools need streaming input
  options: { mcpServers: { shop }, allowedTools: ["mcp__shop__lookup_order"], maxTurns: 6 },
})) { /* handle messages */ }

Python: decorate with @tool("lookup_order", "description", {"order_id": str}), bundle with create_sdk_mcp_server(name="shop", version="1.0.0", tools=[lookup_order]), pass mcp_servers={"shop": server} and allow mcp__shop__lookup_order.

Tool names are always mcp__<server>__<tool>. External MCP servers (stdio or HTTP) go in the same mcpServers option as config objects. Tool design rules from mcp-server-builder apply: task-level tools, bounded schemas, small outputs, isError with a fix-it message.

4. Permissions

MechanismUse
allowedTools / disallowedToolsThe tool surface; anything not allowed needs a permission decision
permissionModedefault, acceptEdits, plan (no edits), bypassPermissions (sandboxed containers only)
canUseTool callbackPer-call decisions in code: inspect input, allow, deny with message, or rewrite input
hooks (PreToolUse etc.)Same event model as Claude Code hooks, as functions
canUseTool: async (toolName, input) => {
  if (toolName === "Bash" && /\b(rm|curl|wget|ssh)\b/.test(String(input.command))) {
    return { behavior: "deny", message: "Network and delete commands are not allowed in this agent." };
  }
  if ((toolName === "Edit" || toolName === "Write") && !String(input.file_path).startsWith("/srv/checkouts/shop/src/")) {
    return { behavior: "deny", message: "Edits are limited to src/." };
  }
  return { behavior: "allow", updatedInput: input };
},

Default to deny-by-omission: list only needed tools, then gate the risky ones. Never ship bypassPermissions outside a disposable sandbox, and confirm with the user before enabling it anywhere.

5. System prompt and settings

  • Without systemPrompt, the SDK uses a minimal prompt, not Claude Code’s. Use { type: "preset", preset: "claude_code", append: "..." } to get Claude Code’s coding behavior plus your rules, or a plain string for a fully custom agent.
  • Filesystem settings are not loaded unless requested. settingSources: ["project"] loads .claude/settings.json and CLAUDE.md; add "user" / "local" only when the agent should inherit a person’s setup (rarely, for services).
  • Skills: load them via settingSources (project .claude/skills/ or user) and include "Skill" in allowedTools.

6. Sessions, subagents, structured output

  • Capture session_id from the system/init message. Continue later with options.resume = sessionId; branch with forkSession: true. Store session IDs with your own job records.
  • Subagents: agents: { reviewer: { description: "...", prompt: "...", tools: ["Read", "Grep"], model: "sonnet" } }. Same design rules as .claude/agents files (see subagent-design).
  • Structured results: instruct an exact JSON shape, extract from result, and validate with Zod/Pydantic; retry once with the validation error appended, then fail.

7. Bound and observe

  • Always set maxTurns; wrap the run in your own wall-clock timeout and abort with an AbortController (TS abortController option) on expiry.
  • Log per run: session_id, num_turns, total_cost_usd, duration, result subtype, tools used. Alert on cost or turn outliers.
  • Pin the model ID and SDK version; prompt or model drift changes behavior silently.

Deployment

  • Run each agent session in an isolated working directory or container; never point cwd at a shared checkout two sessions can edit.
  • Long-lived services: one SDK process per active session, a concurrency cap, and cleanup of temp dirs.
  • Treat any user-supplied or fetched content as untrusted input that may contain instructions; tool gating, not prompt wording, is the real defense.

Gotchas

  • Expecting CLAUDE.md, skills, or project permissions to apply automatically. They do not until settingSources includes the right source.
  • Allowing custom tools by bare name; it must be mcp__<server>__<tool>.
  • Passing a string prompt with in-process SDK MCP tools in TypeScript; use an async iterable prompt.
  • Reading the last assistant text instead of the result message, missing failures like error_max_turns.
  • Migrating from the older Claude Code SDK package: the package name, defaults (system prompt, settings loading), and some option names changed. Re-check each option.

Verification

  1. Happy path: the agent completes the spec’d task; result.subtype === "success".
  2. Permission tests: prompts that try a forbidden edit and a forbidden command are denied with your message, and the agent recovers or stops cleanly.
  3. Bounds: a task designed to loop hits maxTurns and is reported as a failure.
  4. Resume: a second run with resume continues with prior context.
  5. Cost and turns per run are logged and within the budget from the spec.

Output

**Agent:** order-support (TypeScript, @anthropic-ai/claude-agent-sdk, version pinned)
**Tools:** Read, Grep; mcp__shop__lookup_order, mcp__shop__refund_order (canUseTool requires amount <= 5000 cents)
**Prompt:** claude_code preset + support rules; settingSources: ["project"]
**Bounds:** maxTurns 8, 60 s timeout, cost logged per session
**Tests:** happy path, 2 denied actions, max-turns failure, resume - all pass

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc