Guide
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… | Use | Why |
|---|---|---|
| Building an agent without writing the tool loop | Agent SDK | A library that runs the agent loop in your own process, in Python or TypeScript |
| Working interactively, or running one-off tasks from a terminal | Claude Code CLI | The terminal interface, built for daily interactive use |
| Calling the API and implementing the loop yourself | Client SDK | Direct API access rather than access to Claude Code |
| Running long or asynchronous agents without operating a sandbox | Managed Agents | A 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.
| Field | What 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 |
allowedTools | Auto-approve. Silences the prompt; grants nothing that was not already possible |
disallowedTools | Two 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 |
canUseTool | Your 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.
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 for | When | What you give up |
|---|---|---|
| A plain API loop | You want a specific, small tool surface of your own and no filesystem agent at all — a classifier, an extractor, a single structured call | Everything the harness does: file tools, permissions, compaction, sessions. You will rebuild the parts you need |
claude -p | Your program is not Python or TypeScript, the task is one-shot, or you want the agent's answer as JSON from a shell script | Reacting to messages mid-run, programmatic subagents, canUseTool, and in-process control |
| The Agent SDK | You are building something that watches the run: a UI, a queue worker, a service that must react per tool call or hold a session open | Language 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
query(), iterate the messages, read the finalresult. That is the API.allowedToolsauto-approves; it does not restrict.disallowedToolsrestricts.- A scoped deny like
'Bash(rm *)'holds even underbypassPermissions. Use it for the things that must never happen. canUseToolis silently skipped for anything already auto-approved.- A narrower tool surface costs turns and money. Pay it for unattended runs.
- Instrument
total_cost_usdfrom the result message instead of estimating. - If your program is not Python or TypeScript, or the task is one-shot,
claude -pis the smaller answer.
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.