Appendix B: Build Your Own Agent with the Claude Agent SDK
This appendix is for readers who want to embed Claude inside a piece of software they are building — a web service, a scheduled job, a Django backend. If that is not you, you can safely skip this appendix; nothing in the main chapters of the book depends on it.
The rest of this book has treated Claude Code as a tool you sit at and talk to. This appendix treats the same underlying engine as a library — something you can call from your own programs.
What this is, in one paragraph
If you’ve used Claude Code, you’ve already seen what an AI agent can do: read files, run commands, edit code, figure out the sequence of steps required to finish a task. The Claude Agent SDK is that same engine, exposed as a library you can wire into anything you like. You get the same agent loop, the same built-in tools, the same context management. You’re not learning a new model — you’re learning a new front door to it.
🧠 Remember. Agent SDK = Claude Code minus the terminal user interface, plus a programmatic API. Same loop, same tools, same context system. Pick the CLI when you’re at a keyboard; pick the SDK when a piece of software is doing the asking.
When to reach for the SDK
You probably want the SDK if any of these are true:
- You’re building a service — a webhook, a CI job, a scheduled overnight job — that should call Claude as part of its work.
- You want structured output that downstream code can parse without guesswork (this is “JSON Schema enforced output”).
- You need custom tools — your own database, your own internal API — that Claude can call directly.
- You want to run Claude inside a Django view, behind an Azure Function, or inside a clinical pipeline that fires at 2 AM.
You probably do not need it if you’re sitting at a keyboard doing daily development work. That’s what the CLI is for.
A note on the language
The code examples below are in TypeScript, a typed variant of JavaScript that adds type annotations the editor checks for you. It is the language Anthropic ships the SDK in first. Python bindings exist too (pip install claude-agent-sdk) and follow the same patterns one-to-one — so even if your team writes Python, the structure below transfers directly.
Install
# Requires Node.js 18 or newer
npm install @anthropic-ai/claude-agent-sdk # the library
export ANTHROPIC_API_KEY=sk-ant-... # your API key
The first command installs the SDK as a dependency of your project; it bundles the Claude Code engine itself, so there is nothing else to install. The second tells the system your API key — without it, none of the rest will work.
Your first agent
Here is the simplest possible useful example. It asks Claude to list the files in the current directory.
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "What files are in this directory?",
options: {
model: "opus",
allowedTools: ["Glob", "Read"],
maxTurns: 250
}
})) {
if (message.type === "assistant") {
for (const block of message.message.content) {
if ("text" in block) console.log(block.text);
}
}
if (message.type === "result") {
console.log("Done:", message.subtype);
}
}
This is the entire loop. Let’s walk through it line by line, because the for await syntax is unusual.
for await (...) is JavaScript’s way of looping over an asynchronous stream — a stream that produces things one at a time, when they’re ready. query(...) returns such a stream. Each iteration of the loop receives one message from Claude as it arrives. Some messages are Claude’s text replies; some are Claude calling a tool; some are status updates.
query({ prompt, options }) kicks off an agent run. You give it a prompt and a set of options.
allowedTools: ["Glob", "Read"] restricts Claude to only those two tools. Glob lists files matching a pattern; Read reads a file. Claude cannot edit files, run bash, or open a network connection in this run.
maxTurns: 250 caps how many times the agent can iterate before stopping. A high ceiling — most tasks finish in a handful of turns.
Inside the loop, each message has a type. We check for "assistant" (Claude’s reply) and pull out the text. We also check for "result" (the run finished) and print the outcome.
That’s the whole shape. The SDK handles the loop: call the model, see if it wants a tool, execute the tool, feed the result back, repeat until Claude says it’s done.
Message types you’ll see
case "system":
// Session was initialized. Capture session_id if you want to resume later.
if (message.subtype === "init") {
sessionId = message.session_id;
}
break;
case "assistant":
// Claude's text output, or a tool call.
break;
case "result":
// The run is over. message.subtype is "success" or an error type.
// message.total_cost_usd is the dollar spend for this run.
// message.usage has the token counts.
break;
Structured output
For production use — where downstream code has to parse what Claude said — you don’t want free text. You want a JSON object with known fields. You enforce this by passing a JSON Schema:
const reviewSchema = {
type: "object",
properties: {
issues: {
type: "array",
items: {
type: "object",
properties: {
severity: { enum: ["low", "medium", "high", "critical"] },
category: { enum: ["bug", "security", "performance", "style"] },
file: { type: "string" },
line: { type: "number" },
description: { type: "string" },
suggestion: { type: "string" }
},
required: ["severity", "category", "file", "description"]
}
},
summary: { type: "string" },
overallScore: { type: "number" }
}
};
for await (const message of query({
prompt: "Review the code in ./src and identify all issues.",
options: {
model: "opus",
allowedTools: ["Read", "Glob", "Grep"],
outputFormat: { type: "json_schema", schema: reviewSchema }
}
})) {
if (message.type === "result" && message.subtype === "success") {
const review = message.structured_output;
// review.issues is now a typed array. Send it to Slack, a database, anywhere.
}
}
The schema is a contract: Claude’s final output must look exactly like this shape, with these field names and these types. The result is parseable code that downstream programs can rely on. You don’t regex-match on natural language anymore. This is the single biggest unlock the SDK provides for clinical pipelines.
Permission modes
In interactive Claude Code, you approve each tool call. The SDK can run unattended, so you pick a mode upfront:
options: {
permissionMode: "default", // Prompt for approval (interactive only)
// OR
permissionMode: "acceptEdits", // Auto-approve file edits
// OR
permissionMode: "bypassPermissions" // No prompts at all; CI use only
}
For finer control, supply a callback that decides per call:
options: {
canUseTool: async (toolName, input) => {
if (["Read", "Glob", "Grep"].includes(toolName)) {
return { behavior: "allow", updatedInput: input };
}
if (toolName === "Write" && input.file_path?.includes(".env")) {
return { behavior: "deny", message: "Cannot modify .env files" };
}
return { behavior: "allow", updatedInput: input };
}
}
canUseTool is your last line of defense. It runs before every tool call and either allows it, denies it, or modifies the input.
⚠️ Warning. Using
bypassPermissionsin a clinical pipeline that touchesaidi_catalogis a fast way to lose evening sleep. Always wrap withcanUseTooland at minimum block writes to any path containing PHI.
Subagents in the SDK
Same idea as Chapter 13, exposed programmatically. Define your specialists inline:
options: {
allowedTools: ["Read", "Glob", "Grep", "Task"], // Task enables subagents
agents: {
"security-reviewer": {
description: "Security specialist for vulnerability detection",
prompt: "You are a security expert. Focus on injection, secrets, auth.",
tools: ["Read", "Grep", "Glob"],
model: "sonnet"
},
"test-analyzer": {
description: "Test coverage and quality analyzer",
prompt: "You are a testing expert. Find gaps and missing edge cases.",
tools: ["Read", "Grep", "Glob"],
model: "haiku" // cheaper model for simpler work
}
}
}
The main agent decides when to delegate. Each subagent has its own context window, its own tool list, and optionally a cheaper model.
Custom tools via MCP
When the built-in tools aren’t enough, expose your own through MCP (Model Context Protocol — explained in Chapter 15). The example below gives Claude read-only SQL access to aidi_catalog:
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
const aidiServer = createSdkMcpServer({
name: "aidi-db",
version: "1.0.0",
tools: [
tool(
"query_aidi",
"Query aidi_catalog read-only. Returns up to 100 rows.",
{ sql: z.string().describe("SELECT-only SQL") },
async (args) => {
const rows = await runReadOnlyQuery(args.sql);
return { content: [{ type: "text", text: JSON.stringify(rows) }] };
}
)
]
});
for await (const message of query({
prompt: "Find all deceased patients with stage 3 AKI in 2025.",
options: {
model: "opus",
mcpServers: { "aidi-db": aidiServer },
allowedTools: ["mcp__aidi-db__query_aidi"]
}
})) {
// ...
}
You’ve just given Claude one read-only SQL handler. No tool-loop boilerplate; no manual orchestration. This is the pattern for wiring Claude into real KHCC services.
Hooks in the SDK
The same hook system the CLI uses, exposed programmatically:
const blockDangerous: HookCallback = async (input) => {
if (input.hook_event_name === "PreToolUse" && input.tool_name === "Bash") {
const cmd = input.tool_input.command || "";
if (cmd.includes("rm -rf") || cmd.includes("DROP TABLE")) {
return {
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Dangerous command blocked"
}
};
}
}
return {};
};
options: {
hooks: {
PreToolUse: [
{ matcher: "Bash", hooks: [blockDangerous] }
]
}
}
A hook is a function that runs at a defined moment in the agent loop (PreToolUse, PostToolUse, Stop, etc.). It can deny operations, log them, or transform inputs.
🔧 Technical Stuff. Hooks have the signature
(input, toolUseId, { signal }) => Promise<HookResult>. Thesignalis anAbortSignalyou can listen to for cancellation. Return{}to allow the operation to proceed unchanged.
Session resumption
For multi-turn conversations spread across separate HTTP requests, capture the session ID and resume:
let sessionId;
for await (const msg of query({ prompt: "Review the codebase.", options })) {
if (msg.type === "system" && msg.subtype === "init") sessionId = msg.session_id;
}
// Later, in another HTTP request, perhaps an hour later:
for await (const msg of query({
prompt: "Now fix the most critical issue.",
options: { ...options, resume: sessionId }
})) {
// Claude remembers everything from the first call.
}
This is how you build a stateful Claude-backed service without re-sending the full history on every request.
Cost tracking
if (message.type === "result" && message.subtype === "success") {
console.log("Cost:", message.total_cost_usd);
console.log("Tokens:", message.usage);
for (const [model, usage] of Object.entries(message.modelUsage)) {
console.log(`${model}: $${usage.costUSD.toFixed(4)}`);
}
}
The per-model breakdown matters when subagents are in play. You’ll quickly discover that the Haiku-based search agent is essentially free and the Opus reviewer accounts for the entire bill.
A real KHCC pattern
A nightly extraction service for the pathology pipeline:
- An Azure Function fires at 02:00.
- It calls
query()with the extraction prompt, a JSON Schema for the output, and acanUseToolcallback that blocks writes to anything outside anextracts/folder. - A custom MCP server gives Claude read-only schema access to
AIDI-DB. - A subagent named
security-reviewer(running on Sonnet) scans the output for accidental PHI before it’s written anywhere persistent. - A
Stophook runs the eval suite against yesterday’s baseline and blocks the return if anything regressed. - The structured result lands as JSON in blob storage. The downstream pipeline picks it up at 02:30.
No human in the loop. Full audit trail (every tool call logged through a PreToolUse audit hook). Total cost: under $2 per night.
That is what production looks like.
Where to go next
- The official SDK reference. Covers every message type, every option, every tool schema.
- The CLI is the SDK with a UI. Anything you can do interactively in Claude Code you can script with the SDK. Prototype in the CLI, then port the working version to the SDK.
- Skills package up SDK-friendly capabilities. A skill you write at
.claude/skills/<name>/SKILL.mdworks equally well in the CLI and through the SDK’squery().
Closing
You now have the complete picture. Claude Code, the CLI you’ve been using for 22 chapters, is one front door to the agent. The SDK is the other. Same engine; different ergonomics. Pick the one that fits the job in front of you.
The clinical AI office at KHCC will outlive any specific tool, model, or library on this list. Build with that in mind: small, testable, surgical, fail-loud. Claude Code is a force multiplier on those habits. It is not a substitute for them.
Now go ship something.