← All posts

Guide

Building an agent on the Claude Agent SDK

Building an agent on the Claude Agent SDK

The Claude Agent SDK is Claude Code as a library. The same agent loop, the same built-in tools, the same permission system and context handling — running inside your process instead of a terminal. That framing matters, because it tells you what you are and are not signing up for: you are not building an agent, you are embedding one and deciding what it is allowed to touch.

This guide builds a small agent that does something real in a repository, then spends most of its time on the part that actually bites: permissions, when you are the harness and there is no human to approve anything. Every code sample below was run on @anthropic-ai/claude-agent-sdk 0.3.223 — the version published as both latest and next on npm at the time of writing — on Node 22.22.2. The outputs quoted are the outputs we got, including the one that surprised us.

What the SDK gives you that the API does not

Calling the Claude API gives you one model response. If that response asks to read a file, you read the file, append the result, and call again — and you keep doing that until you decide the task is done. That loop is not hard to write badly and quite hard to write well: tool dispatch, parallel calls, error surfaces, context that outgrows the window, a stop condition that is neither premature nor infinite.

The SDK ships that loop, and the tools with it. Anthropic's own comparison is the clearest way to place it:

If you are…UseWhy
Building an agent without writing the tool loopAgent SDKA library that runs the agent loop in your own process, in Python or TypeScript
Working interactively, or running one-off tasks from a terminalClaude Code CLIThe terminal interface, built for daily interactive use
Calling the API and implementing the loop yourselfClient SDKDirect API access rather than access to Claude Code
Running long or asynchronous agents without operating a sandboxManaged AgentsA hosted REST API; Anthropic runs the agent and the sandbox

Two consequences of that table are worth stating plainly. The SDK exists for Python and TypeScript only; from any other language the documented path is to run the CLI as a subprocess with -p and --output-format json. And the SDK loads your project's .claude/ and your ~/.claude/ the same way the CLI does — skills, commands and memory come along whether you were thinking about them or not. If you want a hermetic agent, that is a thing to configure, not a default.

A working agent, from an empty directory

The task: find every TODO comment under src/ and write them to TODO.md, grouped by file. Small enough to paste, real enough that the agent has to search, read and write rather than answer from the prompt.

mkdir sdk-probe && cd sdk-probe
npm init -y && npm pkg set type=module
npm i @anthropic-ai/claude-agent-sdk

Authentication is an ANTHROPIC_API_KEY in the environment. The whole agent is agent.mjs:

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

const q = query({
  prompt: 'Find every TODO comment under src/. Write them to TODO.md, grouped by file. Then stop.',
  options: {
    allowedTools: ['Grep', 'Read', 'Write'],
    permissionMode: 'dontAsk',
    maxTurns: 12,
  },
})

for await (const message of q) {
  if (message.type === 'assistant') {
    for (const block of message.message.content) {
      if (block.type === 'text' && block.text.trim()) console.log('text:', block.text.trim())
      if (block.type === 'tool_use') console.log('tool:', block.name)
    }
  } else if (message.type === 'result') {
    console.log('RESULT', message.subtype, 'turns=' + message.num_turns, 'usd=' + message.total_cost_usd)
  }
}

That is the whole shape of an SDK program. query() returns an async iterator; you loop it. Assistant messages carry content blocks — text for prose, tool_use when the agent reaches for a tool. A single terminal result message carries subtype, num_turns and total_cost_usd. There is no dispatch code because the SDK executes the tools for you.

Run it, and the output is:

text: I'll find the TODO comments under `src/`.
tool: Grep
tool: Grep
tool: Bash
tool: Write
text: Wrote `TODO.md` with 3 TODOs across 2 files: ...
RESULT success turns=5 usd=0.220972

TODO.md is correct. Five turns, twenty-two cents. And there is a Bash in that list, which is the interesting part.

Permissions when you are the harness

allowedTools was ['Grep', 'Read', 'Write']. The agent ran Bash anyway. That is not a bug, and once you read the field description it is not even surprising — but almost everyone reads the name and assumes the opposite.

allowedTools is an auto-approve list, not a sandbox. Tools on it run without a prompt. Tools not on it are still available to the model, and under permissionMode: 'dontAsk' or 'bypassPermissions' they still run. To take a tool away from the agent, name it in disallowedTools.

We checked rather than assumed. Same prompt, same repository, one field added:

    allowedTools: ['Grep', 'Read', 'Write'],
    disallowedTools: ['Bash', 'WebFetch', 'WebSearch'],
    permissionMode: 'dontAsk',
text: I'll search for TODO comments under `src/`.
tool: Grep
tool: Grep
tool: Glob
tool: Glob
tool: Read
tool: Write
RESULT success turns=7 usd=0.44587

No Bash. The agent routed around the missing tool — Glob and Read instead — and still produced a correct TODO.md, in seven turns instead of five, for twice the money. Which is the honest trade of a narrow tool surface: it costs turns, and it is worth it the moment the agent is running unattended against something you care about.

Three more things about the permission chain are worth knowing before you ship anything.

FieldWhat it actually does
permissionMode'default', 'dontAsk', 'plan' or 'bypassPermissions'. In a headless harness there is nobody to answer a prompt, so 'default' will stall on anything not pre-approved
allowedToolsAuto-approve. Silences the prompt; grants nothing that was not already possible
disallowedToolsTwo behaviours in one field. A bare name ('Bash') removes the tool. A scoped rule ('Bash(rm *)') leaves the tool available and denies matching calls — in every permission mode, including bypassPermissions
canUseToolYour own callback for approve/deny decisions. It is invoked only when the permission flow falls through to a prompt — never for a tool already auto-approved by allowedTools, an allow rule, or the permission mode

That last row is the one that produces a bad afternoon. If you write a careful canUseTool that logs every action and blocks the dangerous ones, then set permissionMode: 'bypassPermissions' to stop the agent stalling, your callback goes quiet and you will not be told. The scoped form of disallowedTools is the backstop that survives that mistake, because it is enforced regardless of mode. Use both: scoped denies for the things that must never happen, canUseTool for the judgement calls.

tool call disallowedTools scoped deny wins mode + allowedTools auto-approve canUseTool only if undecided Auto-approve short-circuits your callback. It is not consulted and not told.
The permission chain. Anything decided earlier never reaches your code.

Streaming, and showing progress

The message loop is the progress feed. You do not poll for status; you render what arrives. A tool_use block is the agent starting work, a text block is it narrating, and the single result message ends the run. That is enough to drive a spinner, a log line per tool, or a UI row per file touched.

For multi-turn work, prompt also accepts an AsyncIterable of user messages instead of a string, which keeps one session alive across exchanges:

async function* turns() {
  yield { type: 'user', content: 'Summarise what src/ does.' }
  yield { type: 'user', content: 'Now list the three riskiest functions.' }
}

const q = query({ prompt: turns(), options: { maxTurns: 20 } })
for await (const message of q) { /* render */ }

The returned Query object is more than an iterator. It exposes interrupt() to stop a run in flight, setPermissionMode() and setModel() to change posture mid-session, streamInput() to push more messages into a live query, and close(). If you are building a UI rather than a script, interrupt() is the one you will want first — an agent you cannot stop is an agent people will not run.

SDK, claude -p, or a plain API loop?

This is the decision most posts skip, so here it is directly.

Reach forWhenWhat you give up
A plain API loopYou want a specific, small tool surface of your own and no filesystem agent at all — a classifier, an extractor, a single structured callEverything the harness does: file tools, permissions, compaction, sessions. You will rebuild the parts you need
claude -pYour program is not Python or TypeScript, the task is one-shot, or you want the agent's answer as JSON from a shell scriptReacting to messages mid-run, programmatic subagents, canUseTool, and in-process control
The Agent SDKYou are building something that watches the run: a UI, a queue worker, a service that must react per tool call or hold a session openLanguage choice, and the weight of embedding the whole harness in your process

The honest default is the middle row. A great many "I need to build an agent" problems are one claude -p invocation and some glue, and finding that out costs an afternoon rather than a project. Move up to the SDK when you catch yourself parsing the CLI's JSON to decide what to do next — that is the loop the SDK already has.

The constraint nobody mentions

If you are building a product rather than an internal tool, read this before you design the onboarding. Anthropic's documentation states that unless previously approved, third-party developers may not offer claude.ai login or rate limits for their products, including agents built on the Agent SDK — you authenticate with API keys instead. So "sign in with your Claude subscription and use your own quota" is not a flow you can ship by default, and the token cost lands on you or on a key your user supplies.

There are branding rules alongside it: "Claude Agent" and "{YourName} Powered by Claude" are permitted, "Claude Code" and Claude Code-styled visuals are not. Neither rule is hard to comply with. Both are considerably harder to retrofit after launch.

The short version

Frequently asked questions

What is the Claude Agent SDK?

It is Claude Code exposed as a library. The package is @anthropic-ai/claude-agent-sdk for TypeScript and claude-agent-sdk for Python, and it gives you the same agent loop, built-in tools, permission system, session handling, subagents, hooks and MCP support that the CLI uses. You call query() with a prompt and iterate the messages it yields.

How is the Agent SDK different from calling the Claude API directly?

The API gives you one model response. You write the loop: parse tool calls, execute them, feed results back, decide when the task is finished. The Agent SDK ships that loop plus the tools themselves — file reads, edits, search, shell — along with permissions, context compaction and session persistence. Use the Client SDK instead when you want direct API access and intend to implement the tool loop yourself.

Does allowedTools restrict which tools the agent can use?

No, and this is the most common misreading. allowedTools is an auto-approve list: tools on it run without a prompt. Tools not on it are still available to the model and, under permissionMode: 'dontAsk' or 'bypassPermissions', still run. To remove a tool from the agent entirely, put its bare name in disallowedTools. We verified this by running the same agent twice — the first run called Bash despite Bash being absent from allowedTools.

When should I use claude -p instead of the SDK?

When your program is not in Python or TypeScript, or when the interaction is genuinely one-shot. The docs point non-Python, non-TypeScript callers at running the CLI as a subprocess with -p and --output-format json. Reach for the SDK when you need to react to messages as they stream, drive multi-turn sessions, define subagents programmatically, or intercept tool calls with canUseTool.

Can I ship a product that signs my users in with their Claude subscription?

No. Anthropic states that unless previously approved, third-party developers may not offer claude.ai login or rate limits for their products, including agents built on the Claude Agent SDK; you use API key authentication instead. There are also branding rules — you may say "Claude Agent" or "Powered by Claude", but not "Claude Code".

How much does a small SDK agent cost to run?

Our example agent — find every TODO under src/ and write them to a file, in a toy repository with two source files — completed in 5 turns for $0.22 and, on a second run with a narrower tool surface, 7 turns for $0.45. The total_cost_usd field on the final result message reports it per run, which is the number to instrument rather than estimate.

Where to go next

Subagents are the SDK's answer to a task too big for one context window, and the subagents explainer covers the model the agents option exposes programmatically. If your agent needs data the built-in tools cannot reach, that is an MCP server — either one that already exists or one you write. And the constraint that shapes every long-running agent is the window itself, which the guide to managing context in a long session takes apart. For running the CLI unattended rather than embedding it, see running Claude Code in the background.

Sources

Verified against @anthropic-ai/claude-agent-sdk 0.3.223, published on npm as both latest and next at the time of writing, running on Node 22.22.2. The query(), startup() and tool() signatures, the Options fields, the Query methods and the note that canUseTool is not invoked for auto-approved tools: Agent SDK TypeScript reference. The comparison against the CLI, Client SDK and Managed Agents, the Python/TypeScript-only note, the capability list, the claude.ai login restriction and the branding rules: Agent SDK overview. Running the CLI from other languages with -p and --output-format json: headless mode. The two runs quoted — tool sequences, turn counts and total_cost_usd — are ours, executed against a two-file toy repository; they are an illustration of the mechanism, not a benchmark, and your costs will differ with repository size and model.

Where Backgrind fits

An agent you built is an agent that runs somewhere you are not looking. Everything above about permissions exists because an unattended run either stalls waiting for an answer nobody is there to give, or does not stall and you find out afterwards. Backgrind takes the other side of that problem for the CLI you already use: it runs your real Claude Code in an always-on-top overlay and turns a held permission prompt into an ambient toast you can approve with one keystroke, so the decision reaches you instead of the run quietly stopping. Build the harness; keep a human reachable. See the loop in the live demo.