From script to your own agent: the Claude Agent SDK in 90 minutes
You know Claude Code as a CLI. With the Agent SDK you build the same agentic loop into your own code, with tool control, budget limits and MCP integration. Step by step.
You know Claude Code as the thing in the terminal. You type, it reads files, writes code, asks questions. What many people do not know is that exactly this agentic loop sits in an npm package you can pull into your own project. The Claude Agent SDK. With it you do not build a chatbot that only returns text, but an agent that uses tools, touches files, plans several steps and delivers a result at the end. In this playbook we build, step by step, a small agent that checks a file for bugs and fixes them itself. Afterwards you know how to allow tools, cap costs and attach an MCP server. You need Node 20 or newer and an Anthropic API key.
1. When the SDK is worth it, and when not
Before you install, an honest framing. The SDK is not "Claude Code but as a library for chatting". It is the agentic loop without the terminal UI around it. You take it when you want to build agent behaviour into something of your own, a CI job that reviews PRs, a nightly cron that goes through logs, an internal tool that works through tickets. If you only want to send a single prompt and get an answer, without tools, without multiple steps, then Anthropic's normal Messages API is the simpler choice. The SDK only shines once the agent has to think more than once and do something along the way. I notice that most people reach for the SDK too early, for things a simple API call could handle too.
2. Install it and the first sign of life
Create an empty project and install the package. The current state is version 0.3.162.
npm install @anthropic-ai/claude-agent-sdk
export ANTHROPIC_API_KEY=sk-ant-...
Then the shortest agent that exists. A file agent.mjs:
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Explain in two sentences what this codebase does.",
options: {
maxTurns: 1,
allowedTools: ["Read", "Grep"]
}
})) {
if (message.type === "result") {
console.log(message.result);
}
}
query() is not a normal function call that returns an object. It is an async iterator. You walk with for await through a stream of messages while the agent works. That is exactly what makes it an agent and not a chat endpoint.
3. Really understand the agentic loop
Every message from the iterator has a type. You need three of them at the start. assistant is Claude thinking or calling a tool. result is the final result at the end. In between many assistant messages can come, each one a step in the loop. This is how you read both:
for await (const message of query({ prompt, options })) {
if (message.type === "assistant" && message.message?.content) {
for (const block of message.message.content) {
if ("text" in block) {
console.log(block.text); // Claude's reasoning
} else if ("name" in block) {
console.log(`Tool: ${block.name}`); // which tool is running
}
}
} else if (message.type === "result") {
console.log(`Done: ${message.subtype}`);
}
}
Once you see that run, you understand the whole SDK. Claude thinks, calls a tool, gets the result back, thinks on, until it is done. You only watch and decide what it may touch.
4. Allow and block tools
The most important lever is allowedTools. Leave it out and Claude may use all built-in tools, including Bash and Write. You do not want that at the start. State explicitly what is allowed:
options: {
allowedTools: ["Read", "Edit", "Glob", "Grep"],
disallowedTools: ["Bash"]
}
The built-in tool names are the same as in Claude Code: Read, Write, Edit, Glob, Grep, Bash. A rule of thumb that has served me well: start with read-only tools, Read, Grep, Glob, and just watch the agent first. Only once the behaviour fits do you add Edit. You give Bash last and only if you really must, because with it the agent can run arbitrary commands.
5. Permission mode, the safety screw
Allowing tools is one half, the other is who does the clicking. For that there is permissionMode. The default is 'default', where the agent asks on delicate actions, which in a script without a human in front leads to hanging. For unattended runs you take 'acceptEdits', then file edits are waved through automatically and the rest stays controlled:
options: {
allowedTools: ["Read", "Edit", "Glob"],
permissionMode: "acceptEdits"
}
There is also 'bypassPermissions', which really waves everything through. For that you additionally have to set allowDangerouslySkipPermissions: true, and the name says it all. Only take that in a sandbox or a throwaway container, never on your real machine with real files. I ran that locally once and wrecked a git branch, since then only in Docker.
6. Control the system prompt
By default the SDK runs with an empty system prompt, so your agent is a blank slate. Two ways to change that. Either you give your own string and control everything yourself:
options: {
systemPrompt: "You are a Python specialist. Write type hints, short docstrings, no comments explaining the obvious."
}
Or you take Claude Code's behaviour as the base and only append your addition:
options: {
systemPrompt: {
type: "preset",
preset: "claude_code",
append: "Stick to our ESLint config and never commit directly to main."
}
}
The preset gives you the entire Claude Code behaviour, file conventions, caution around destructive actions, the lot. For most build agents the preset plus append is a better start than an empty prompt of your own.
7. Cap budget and turns
An agent that thinks in a loop can get expensive if it runs away with itself. You always put in two limits. maxTurns bounds how often the loop runs, maxBudgetUsd stops at a dollar amount:
options: {
maxTurns: 12,
maxBudgetUsd: 0.50
}
On a nightly job that runs across 30 files, those two lines are exactly the difference between "costs 40 cents" and "accidentally costs 12 dollars". Always set them, even when experimenting. If you want Opus with long reasoning for hard tasks, you can define a cheaper backup via model and fallbackModel in case the main model is unavailable.
8. Attach an MCP server
This is where it gets interesting. Your agent can use not only the built-in tools but any MCP server you connect. That gives it access to your database, your memory, your internal API. You pass the server configs in via mcpServers:
options: {
mcpServers: {
memory: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-memory"]
}
},
allowedTools: ["Read", "Grep", "mcp__memory__search"]
}
Important: an MCP server's tools are called mcp__<servername>__<toolname> and have to appear exactly like that in allowedTools, otherwise the agent may not touch them. If you do not have your own MCP server yet, build that first, we have a separate playbook for it (see below). With MCP the generic code agent becomes an agent that knows your stack.
9. Define subagents programmatically
For bigger tasks you do not want one agent that does everything, but several with clear roles. The SDK lets you define subagents directly in code, via the agents option. Each gets its own description and its own tools:
options: {
agents: {
reviewer: {
description: "Reads code and finds bugs, but never writes itself.",
tools: ["Read", "Grep", "Glob"]
},
fixer: {
description: "Fixes the bugs the reviewer reported.",
tools: ["Read", "Edit"]
}
}
}
That is the same pattern you know from the CEO worker pattern, only cast in code instead of markdown files. The main agent delegates to the subagents, each stays in its lane. Keeping a reading reviewer and a writing fixer separate prevents an agent analysing wrongly and immediately fixing wrongly in one go.
10. Turn the script into a real job
The last step turns the toy into a service. Three things you need for that. First error handling, the result type has a subtype that tells you whether it worked or whether the agent ran into a limit, you have to evaluate that instead of simply ignoring it. Second an AbortController in case you want to be able to cancel the run from outside. Third, for a nightly job, a thin wrapper that logs the output and sends a message on errors. If your agent should run regularly, combine it with scheduled agents, then you have an agent that works every night on its own. And if you need 1M context because your agent reads large codebases, you enable that via betas: ["context-1m-2025-08-07"] in the options.
With that you have everything you need: tool control, permission mode, budget limit, MCP integration and subagents. The rest is fine-tuning for your concrete use case.
What next
Once you have the first agent running, what you are probably missing is your own MCP server that knows your stack. For that First MCP server in 90 minutes is the next step. For the cost side, Claude Code cost controls for daily drivers is worth it, the principles apply one to one to SDK agents. And if your agent should run by itself, look at Scheduled agents with routines. The basics on the term agent are in Level 5, what is an agent, the CEO worker pattern from step 9 is deepened in CEO worker pattern.
Source
The options and examples used here come from the official Agent SDK docs, https://docs.claude.com/en/api/agent-sdk/typescript and https://docs.claude.com/en/api/agent-sdk/overview. Package version 0.3.162 verified via npm, npm view @anthropic-ai/claude-agent-sdk version. The Python variant is called claude_agent_sdk with query, ClaudeAgentOptions, AssistantMessage and ResultMessage.