The Claude Agent SDK: Building agents in TypeScript

By Pugazhenthi

Generic badgeGeneric badge

Most developers reaching for the Anthropic API end up rebuilding the same tool-calling loop, badly. The Claude Agent SDK is the layer above that — the same harness that powers Claude Code, exposed as a TypeScript library that runs inside your own process. If you can write a Node script, you can ship an agent.

This post walks the pieces you actually need to know. One idea per section, one TypeScript snippet each.

1. What the SDK actually is

There are four things people conflate. The CLI is the interactive terminal tool. The Client SDK is the raw API where you write the tool loop yourself. The Agent SDK is the loop already written, running in your process. Managed Agents is Anthropic running the whole thing for you.

If you find yourself building “a while loop that sends messages, reads tool calls, dispatches them, appends results, sends again” — stop. That’s what the Agent SDK is.

npm install @anthropic-ai/claude-agent-sdk

2. The agent loop

An LLM answers. An agent decides when it’s done. The SDK gives you a query() function that returns an async iterable — every message the agent produces, including its tool calls and their results, streams out as it happens.

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

for await (const msg of query({
  prompt: "Find every TODO in ./src and summarize what's still open.",
})) {
  if (msg.type === "assistant") {
    console.log(msg.message.content);
  }
}

The intelligence isn’t in any single call. It’s in the feedback loop between the model’s plan and what the tool results actually said.

3. Built-in tools

Out of the box the agent has Read, Write, Edit, Bash, Glob, Grep, and WebSearch. That means it can navigate a filesystem, run commands, and search the web on turn one — no wiring required.

The pattern that matters: the filesystem is the agent’s memory. Agents that write intermediate results to disk and read them back scale. Agents that try to keep everything in context hit a wall.

for await (const msg of query({
  prompt:
    "Read package.json, list outdated deps, write the report to deps-report.md.",
  options: {
    allowedTools: ["Read", "Bash", "Write"],
  },
})) {
  // stream events
}

4. Custom tools

The gap between a chatbot and an agent is one function you let it call. Custom tools are defined as an in-process MCP server — Claude calls your TypeScript function directly, no HTTP hop.

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

const getWeather = tool(
  "get_weather",
  "Get current weather for a city. Use for questions about temperature or conditions.",
  { city: z.string().describe("City name, e.g. 'Trichy'") },
  async ({ city }) => {
    const res = await fetch(`https://api.weather.example/${city}`);
    const data = await res.json();
    return {
      content: [{ type: "text", text: `${data.temp}°C, ${data.summary}` }],
    };
  },
);

const weatherServer = createSdkMcpServer({
  name: "weather",
  version: "1.0.0",
  tools: [getWeather],
});

The tool description is what the model actually reads to decide when to call it. Write it for a new hire, not a compiler. Vague description, wrong tool.

5. Permissions and hooks

Your agent can run rm -rf. Whether it does is a config decision.

Permissions are the fence. You declare which tools run silently, which need approval, and which are outright denied. Match on tool parameters, not just tool names — allowing Bash broadly is very different from allowing Bash(git status).

for await (const msg of query({
  prompt: "Format the codebase.",
  options: {
    allowedTools: ["Read", "Edit", "Bash"],
    permissionMode: "acceptEdits",
    disallowedTools: ["Bash(rm:*)", "Bash(sudo:*)"],
  },
})) {
  // ...
}

Prompts are suggestions; hooks are guarantees. When a rule must hold — auto-format after every write, block writes outside a directory, log every tool call — enforce it in code, not in the system prompt.

6. Subagents

The fix for a bloated context window isn’t a bigger model. It’s a second agent. A subagent gets its own context, own prompt, own tool set — and returns a conclusion, not a transcript.

for await (const msg of query({
  prompt: "Audit the auth flow and report any issues.",
  options: {
    agents: {
      "security-review": {
        description:
          "Reads code and reports security issues. Use for security audits.",
        prompt:
          "You are a security reviewer. Read files, identify issues, return a bullet list.",
        tools: ["Read", "Grep", "Glob"],
      },
    },
  },
})) {
  // ...
}

The parent agent decides when to delegate. You just tell it the tool exists.

7. Structured outputs

“Respond only in JSON” is not an API contract. If the agent’s answer is going into a database row or an API response, force it into a schema.

import { z } from "zod";

const InvoiceSummary = z.object({
  vendor: z.string(),
  total: z.number(),
  currency: z.string(),
  dueDate: z.string(),
});

let result: z.infer<typeof InvoiceSummary> | null = null;

for await (const msg of query({
  prompt:
    "Extract vendor, total, currency, and due date from ./invoice.pdf as JSON.",
  options: { allowedTools: ["Read"] },
})) {
  if (msg.type === "result" && msg.subtype === "success") {
    result = InvoiceSummary.parse(JSON.parse(msg.result));
  }
}

Structured output is what turns an agent into a component you can drop into a bigger pipeline.

8. Getting to production

The agent loop is the easy part. What you add once real users depend on it:

  • Cost tracking — token usage is per-turn, not per-request. Log it or you’ll be surprised.
  • Observability — OpenTelemetry traces for every tool call. When something goes wrong, you want to see the exact tool sequence, not guess.
  • Isolation — one session per tenant, sandbox the filesystem, scope credentials.
  • Timeouts and budgets — cap max turns, cap max tokens, cap wall-clock time. An agent left to think forever will.
  • A denylist that isn’t a hopeBash(*) allowed with a system-prompt warning to “be careful” is not a security control.

That’s the shape of it. The SDK ships more than this — MCP servers you didn’t build, agent skills, session forking, streaming, context compaction — but the pieces above are enough to put a real agent in front of users.