Building your own agent with the Agent SDK
Drive the agent loop from your own code: define tools, control permissions programmatically, and decide when building beats shelling out to the CLI.
When to build rather than drive the CLI
The CLI is an agent application built on the Agent SDK. Shelling out with -p covers scripts and CI. Building on the SDK is worth it when you need the loop inside your own process: a custom UI, per-request tool policy, tools that only exist in your application, or programmatic permission decisions.
| Requirement | CLI with -p | Agent SDK |
|---|---|---|
| Scheduled review in CI | Good fit | Unnecessary |
| Custom permission logic per user | Awkward | Good fit |
| Tools backed by your application | Via MCP server | Direct |
| Embedded in a product surface | Poor fit | Good fit |
| Streaming UI you control | Limited | Good fit |
npm install @anthropic-ai/claude-agent-sdk
# or, for Python
pip install claude-agent-sdkThe shape of an SDK call
import { query } from "@anthropic-ai/claude-agent-sdk";
const messages = query({
prompt: "Find the place where webhook signatures are verified and explain the failure modes.",
options: {
cwd: process.cwd(),
maxTurns: 8,
allowedTools: ["Read", "Grep", "Glob"],
permissionMode: "default",
settingSources: [], // do not inherit CLAUDE.md or user settings
systemPrompt:
"You audit code. Report findings as a list; never modify files.",
},
});
for await (const message of messages) {
if (message.type === "assistant") {
for (const block of message.message.content) {
if (block.type === "text") process.stdout.write(block.text);
}
}
if (message.type === "result") {
console.log("\n---");
console.log("turns:", message.num_turns, "cost:", message.total_cost_usd);
}
}queryreturns an async iterable of messages: assistant turns, tool calls and a final result message.settingSourcescontrols whether filesystem settings are loaded. An empty array means the run is defined entirely by your code - which is what you want in a server process.maxTurnsandallowedToolsare your cost and safety controls, exactly as in the CLI.- The result message carries the same fields as the CLI's JSON output: turns, duration, cost and an error flag.
Custom tools and evaluation
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
const lookupOrder = tool(
"lookup_order",
"Fetch an order by id from the internal API.",
{ orderId: z.string().describe("Order id, e.g. A-1042") },
async ({ orderId }) => {
const res = await fetch(process.env.API + "/orders/" + orderId);
if (!res.ok) {
return { content: [{ type: "text", text: "not found" }], isError: true };
}
return { content: [{ type: "text", text: await res.text() }] };
}
);
const server = createSdkMcpServer({ name: "internal", tools: [lookupOrder] });
const result = query({
prompt: "Why did order A-1042 fail?",
options: { mcpServers: { internal: server }, maxTurns: 6 },
});Then measure it. The transcript, the turn count and the cost are the only evidence that a prompt or tool change helped rather than merely felt better.
- Collect twenty real requests with the answer you would accept, written down before you run anything.
- Run them headlessly and record the transcript, the turn count and the cost for each.
- Score the final answer, not the path - an inefficient run that lands correctly beats an elegant run that does not.
- Track turn count as a proxy for cost and a symptom of confusion; a task that suddenly takes three times the turns has regressed.
- Re-run the set after any prompt or tool change; without a fixed set you are comparing anecdotes.
const cases = [
{ q: "Why did order A-1042 fail?", expect: ["declined", "insufficient funds"] },
{ q: "Which orders are stuck in review?", expect: ["A-1030"] },
];
for (const c of cases) {
const answer = await runOnce(c.q);
const ok = c.expect.some((e) => answer.text.toLowerCase().includes(e));
console.log(ok ? "pass" : "FAIL", "|", c.q, "| turns:", answer.numTurns);
}Keep the harness in the repository next to the prompt. The prompt is the program; the evaluation is its test suite.
FAQ
Should I use the SDK or just call the model API directly?
Why does my SDK agent behave differently from my CLI session?
settingSources loads none of that. State the behaviour you want in systemPrompt rather than relying on files the process may not see.Related
Headless mode, scripts and CI automation Connecting external tools with MCP servers
Last refreshed 2026-09-18.