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.

RequirementCLI with -pAgent SDK
Scheduled review in CIGood fitUnnecessary
Custom permission logic per userAwkwardGood fit
Tools backed by your applicationVia MCP serverDirect
Embedded in a product surfacePoor fitGood fit
Streaming UI you controlLimitedGood fit
npm install @anthropic-ai/claude-agent-sdk
# or, for Python
pip install claude-agent-sdk

The 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);
  }
}
  • query returns an async iterable of messages: assistant turns, tool calls and a final result message.
  • settingSources controls 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.
  • maxTurns and allowedTools are 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 },
});
⚠️
A tool result is untrusted input the moment it comes from outside your process. Text returned by an API can contain instructions that the model may follow. Validate arguments before acting, scope credentials to the single operation, and never let a tool result authorise a destructive follow-up call.

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.

  1. Collect twenty real requests with the answer you would accept, written down before you run anything.
  2. Run them headlessly and record the transcript, the turn count and the cost for each.
  3. Score the final answer, not the path - an inefficient run that lands correctly beats an elegant run that does not.
  4. Track turn count as a proxy for cost and a symptom of confusion; a task that suddenly takes three times the turns has regressed.
  5. 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?
Call the API directly when you need one model response with no tools and no iteration. Use the SDK when you need the loop: tool calls, permission decisions, context management, subagents and session resumption. Rebuilding that loop by hand is where most of the engineering effort disappears.
Why does my SDK agent behave differently from my CLI session?
The defaults differ. The CLI loads instruction files, settings and MCP configuration from disk; an SDK run with empty settingSources loads none of that. State the behaviour you want in systemPrompt rather than relying on files the process may not see.

Headless mode, scripts and CI automation Connecting external tools with MCP servers

Last refreshed 2026-09-18.