If you searched “build AI agent with Claude Code”, you probably want one of two things: to turn Claude Code itself into a worker that runs your business tasks on a schedule, or to put the same engine inside your own Python or TypeScript app. Both work, and they share one engine. Anthropic’s Claude Agent SDK gives you “the same tools, agent loop, and context management that power Claude Code, programmable in Python and TypeScript” (Agent SDK overview). The hard part is not the first demo. It is permissions, cost control, hosting and knowing when a simpler route fits better.
This guide is written for founders, ops leads and the people who build automations for them. It covers the four ways to build, a full working example of a support-ticket triage agent with a custom tool, a safety hook and a spend cap, a cost model built from Anthropic’s published prices, hosting, the mistakes that waste the most money, and a launch checklist. Every number links to its source.
What it means to build an AI agent with Claude Code
An agent, in Anthropic’s own definition, is “an application that completes a task by planning its own steps and calling tools that read files, run commands, or edit code” (Agent SDK overview). The difference from a chatbot is the loop: the model reads the task, picks a tool, looks at the result, and decides what to do next until the job is done or a limit is hit. Claude Code is that loop wrapped in a terminal interface. The Agent SDK is the same loop exposed as a library.
When you call the SDK, it does not reimplement the agent in your language. Per the hosting guide, your code calls query(), and the SDK “spawns a separate claude CLI process and talks to it over stdio.” That subprocess owns the shell, the working directory and the session transcripts on disk. This one fact explains most of the design decisions later in this article: why each agent needs its own working directory, why memory grows over long sessions, and why hosting an agent is not like hosting a stateless API.
Out of the box the agent gets the capabilities Claude Code has: built-in tools to read, write and edit files, run commands and search the web; hooks; subagents; MCP connections to outside systems; permissions; sessions; and skills and memory loaded from a project’s .claude/ folder (Agent SDK overview). You do not write the tool loop, retries or context compaction yourself. You decide what the agent may touch, what it should do, and when it must stop.
Anthropic’s long-standing advice still applies before you write any code: use the simplest pattern that works, and add autonomy only when a fixed workflow cannot handle the variation in the task (Anthropic: Building effective agents). Many “agents” are really a prompt plus one or two tool calls, and that is fine.
Four ways to build, and which one to pick
Anthropic now offers four routes to the same kind of agent. They differ in who runs the loop, what is built in, and how much code you write. The table summarises the official comparison (Agent SDK overview) with the practical trade-offs we see when building for small and mid-sized businesses.
| Route | Who runs the loop | Code you write | Best for | Main limit |
|---|---|---|---|---|
Claude Code CLI with CLAUDE.md, skills and claude -p |
The claude binary on your machine or CI runner |
Almost none: markdown files and a shell command | Internal jobs run by one team: reports, content, repo upkeep, scheduled checks | No custom UI; harder to serve many end users |
| Claude Agent SDK (Python or TypeScript) | A CLI subprocess your app spawns and supervises | A small app: options, custom tools, hooks | Agents inside your product, internal tools with a UI, multi-tenant services | You host it: containers, session storage, isolation |
| Managed Agents | Anthropic’s hosted harness, in a cloud or self-hosted sandbox | API calls to configure and drive sessions | Teams that want the agent loop without running servers | Runtime fee on top of tokens; less control of the machine |
| Client SDK (raw Messages API) | Your own code | The whole tool loop, or the beta tool runner | Narrow, high-volume calls where you want full control of every request | You rebuild what the other three give you free |
A quick rule: if the agent’s users are your own team and the work lives in files or a repository, start with the CLI. If outsiders will use it, or it must sit behind your own interface, use the Agent SDK. If you do not want to run infrastructure at all, price out Managed Agents. Drop to the raw Client SDK only when you have a reason to own every request. If you are weighing Claude Code against other coding agents first, our Claude Code vs Codex comparison covers that choice.
Who this approach is for, and who should skip it
Building on Claude Code fits teams that have at least one person who can read a stack trace and edit a config file. It shines when the task involves files, documents, spreadsheets, code or web research, because those are the built-in tools. Typical good fits we see at agency clients: triaging support tickets, drafting weekly operations reports from CSV exports, reconciling invoices against a ledger, refreshing product descriptions, researching leads before a sales call, and keeping a content calendar moving.
It is a weaker fit in three cases. First, if your process is a fixed sequence with no judgment (“when a form is submitted, add a row and send an email”), a low-code tool like n8n, Make or Zapier is cheaper and easier to hand over. Second, if nobody on the team can own a Python or Node process in production, the SDK route will stall at deployment; the CLI route or a hosted option is more realistic. Third, if the agent must act on behalf of customers using their Claude accounts, note that Anthropic does not allow third-party developers to offer claude.ai login or rate limits in their products without prior approval; products must use API key authentication (Agent SDK quickstart).
There is also a licensing point that matters for agencies building for clients. Use of the Agent SDK is governed by Anthropic’s Commercial Terms of Service, including when you use it to power products for your own customers, and your product may not be branded as “Claude Code” or mimic Claude Code’s visuals; “Powered by Claude” style naming is allowed (Agent SDK overview: branding and license). Settle naming before you design the interface, not after.
Route 1: turn Claude Code itself into the agent
The fastest agent is often no new code at all. Claude Code reads standing instructions from CLAUDE.md, loads procedures from skills in .claude/skills/, delegates side work to subagents, and enforces hard rules with hooks. Put those in a repository, then run Claude Code non-interactively with -p, and you have an agent that runs on a schedule. If you are unsure which building block a rule belongs in, read our guide to Claude Code hooks vs skills vs subagents first.
The non-interactive commands you will use most, all from the headless mode docs:
# One task, tools pre-approved, JSON result with cost
claude --bare -p "Summarize this week's tickets in reports/ into summary.md" \
--allowedTools "Read,Write,Glob" \
--output-format json
# Structured output that a script can trust
claude -p "Extract every invoice number and total from inbox/" \
--output-format json \
--json-schema '{"type":"object","properties":{"invoices":{"type":"array","items":{"type":"object","properties":{"number":{"type":"string"},"total":{"type":"number"}},"required":["number","total"]}}},"required":["invoices"]}' \
| jq '.structured_output'
# Unattended run: nothing waits for a human to approve a prompt
claude -p "Update the dependency pins and run the tests" \
--permission-mode auto --permission-prompts none
Three details make this reliable. --bare skips auto-discovery of hooks, skills, plugins, MCP servers and CLAUDE.md, so a run behaves the same on every machine; Anthropic calls it “the recommended mode for scripted and SDK calls” and says it will become the default for -p (headless docs). Leave --bare off when you want the repository’s skills and CLAUDE.md to load, as in a scheduled agent repo. --json-schema puts validated output in a structured_output field, which you should check exists before use. And with --output-format json the response includes total_cost_usd, so every run can log what it cost.
We run our own content operation this way: a repository with a CLAUDE.md of facts, three skills (daily writing, weekly keyword refill, weekly review) and scheduled sessions that commit their records back to git. No server, no database. The trade-off is that only the team that owns the repo can use it, which is exactly right for internal work.
How to build an AI agent with Claude Code: SDK step by step
When the agent needs to live inside your own app, use the SDK. The walkthrough below builds a support-ticket triage agent: it reads new tickets from a folder, looks up each customer’s plan through a custom tool, assigns a priority and a queue, and writes a triage file. It cannot touch anything outside its folder and stops at a dollar limit. Python is shown; TypeScript is nearly line-for-line the same.
Step 1: install and authenticate
You need Python 3.10+ or Node.js 18.14+, and both SDKs bundle a native Claude Code binary, so most installs need no separate Claude Code install (quickstart).
mkdir triage-agent && cd triage-agent
python3 -m venv .venv && source .venv/bin/activate
pip install claude-agent-sdk
export ANTHROPIC_API_KEY=your-api-key # from the Claude Console
# TypeScript equivalent
npm init -y && npm pkg set type=module
npm install @anthropic-ai/claude-agent-sdk zod
npm install --save-dev tsx
The SDK reads the key from the process environment and does not load .env files on its own. It also runs on Amazon Bedrock (CLAUDE_CODE_USE_BEDROCK=1), Google Cloud (CLAUDE_CODE_USE_VERTEX=1) and Microsoft Foundry (CLAUDE_CODE_USE_FOUNDRY=1) if your company already buys models there (quickstart).
Step 2: run the smallest possible agent
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def main():
async for message in query(
prompt="List the files in tickets/ and say how many there are.",
options=ClaudeAgentOptions(allowed_tools=["Read", "Glob"]),
):
if isinstance(message, ResultMessage):
print(message.subtype, message.result, message.total_cost_usd)
asyncio.run(main())
query() returns an async stream. Each item is a step: Claude’s reasoning, a tool call, a tool result, or the final ResultMessage. The loop ends when the task is done or an error or limit is hit (quickstart). Get this running before you add anything else; it proves your key, network and binary all work.
Step 3: give the agent a custom tool
Custom tools are how the agent reaches your business systems. You define a name, a description Claude reads to decide when to call it, an input schema and an async handler, then wrap the tools in an in-process MCP server (custom tools docs).
import csv
from typing import Any
from claude_agent_sdk import tool, create_sdk_mcp_server
@tool(
"lookup_customer",
"Look up a customer's plan and account owner by email address",
{"email": str},
)
async def lookup_customer(args: dict[str, Any]) -> dict[str, Any]:
with open("data/customers.csv") as f:
for row in csv.DictReader(f):
if row["email"].lower() == args["email"].lower():
return {"content": [{"type": "text",
"text": f"plan={row['plan']} owner={row['owner']}"}]}
# A composed error tells Claude what to do next
return {"content": [{"type": "text",
"text": f"No customer found for {args['email']}. Treat as plan=free."}],
"is_error": True}
crm = create_sdk_mcp_server(name="crm", version="1.0.0", tools=[lookup_customer])
Swap the CSV for your real CRM’s API later. Two habits from the docs pay off immediately: return is_error with a helpful message instead of throwing, because Claude reads that message and can recover, and keep descriptions specific, because they are the only thing Claude uses to decide when the tool applies.
Step 4: lock down permissions and add a safety hook
from claude_agent_sdk import HookMatcher
import os
async def stay_in_workspace(input_data, tool_use_id, context):
path = input_data["tool_input"].get("file_path", "")
rel = os.path.relpath(os.path.abspath(path)) if path else ""
if rel and not rel.startswith(("tickets/", "out/", "data/")):
return {"hookSpecificOutput": {
"hookEventName": input_data["hook_event_name"],
"permissionDecision": "deny",
"permissionDecisionReason": "Only tickets/, out/ and data/ are allowed",
}}
return {}
options = ClaudeAgentOptions(
system_prompt=("You triage support tickets. For each ticket, look up the "
"customer, assign P1-P4 and a queue (billing, bug, how-to), "
"and write one line per ticket to out/triage.md."),
mcp_servers={"crm": crm},
tools=["Read", "Glob", "Write"], # only these built-ins exist
allowed_tools=["Read", "Glob", "Write", "mcp__crm__lookup_customer"],
permission_mode="dontAsk", # anything else is denied
hooks={"PreToolUse": [HookMatcher(matcher="Write|Edit|Read",
hooks=[stay_in_workspace])]},
setting_sources=[], # ignore host settings files
max_turns=40,
max_budget_usd=2.0,
)
Each line has a reason. tools removes every built-in you did not list, including Bash, so Claude never even attempts a shell command (custom tools: control tool access). dontAsk turns every call that would prompt into a denial, which is what you want in a headless service. The hook runs before every other permission step, and a hook deny applies even in bypassPermissions mode (permissions docs). max_turns matters because the SDK has no top-level session timeout (hosting: known limitations), and max_budget_usd ends the run with an error_max_budget_usd result once estimated spend reaches the cap (subagents: cap spend).
Step 5: run it and read the result
async def main():
try:
async for message in query(prompt="Triage every ticket in tickets/.",
options=options):
if isinstance(message, ResultMessage):
print(message.subtype, f"${message.total_cost_usd or 0:.4f}")
except Exception as error:
print(f"Run failed: {error}") # raised after an error result
asyncio.run(main())
A single-shot query() raises after it yields an error result such as error_max_turns, so always wrap the loop (hosting guide). Read out/triage.md, compare it to how a human would have triaged ten tickets, and adjust the system prompt before you add volume.
Step 6: add a specialist subagent when the main prompt gets crowded
If triage needs a second opinion on refund requests, define a subagent instead of stuffing more rules into one prompt. Subagents run in their own context, can have a narrower tool list and a cheaper model, and return only their final message (subagents docs).
from claude_agent_sdk import AgentDefinition
options.agents = {
"refund-reviewer": AgentDefinition(
description="Reviews refund requests against the refund policy.",
prompt="Read data/refund-policy.md and decide approve, deny or escalate.",
tools=["Read"],
model="haiku",
)
}
options.tools.append("Agent") # make the tool available
options.allowed_tools.append("Agent") # and pre-approve it
Claude invokes subagents through the Agent tool, so it must be in the tools list you set in step 4 and in allowed_tools. Keep subagent counts low at first; each one makes its own API requests, and all of them count toward the run’s cost.
Costs and limits, with a worked example
Token spend is the cost that matters. Anthropic’s hosting guide says token cost “typically dominates container infrastructure cost by an order of magnitude or more,” with a minimally provisioned container at roughly $0.05 per hour (hosting guide: cost). Current list prices per million tokens, from Anthropic’s pricing page:
| Model | Input | Cache hits | Output |
|---|---|---|---|
| Claude Opus 5.5 | $4 | $0.20 | $20 |
| Claude Sonnet 5.5 | $2 | $0.10 | $10 |
| Claude Haiku 5.5 (prompts up to 100,000 tokens) | $0.10 | $0.01 | $0.50 |
The same page lists web search at $10 per 1,000 searches plus tokens, a 50 percent Batch API discount, and, for Managed Agents, a session runtime fee of $0.08 per session-hour on top of tokens (pricing).
Worked example (our assumptions, not a benchmark): the triage agent handles 200 tickets a day. Each run reads about 40,000 input tokens (system prompt, tool definitions, ticket text, tool results) and writes about 3,000 output tokens.
- Sonnet 5.5, no caching: 40,000 × $2/M = $0.08 input, plus 3,000 × $10/M = $0.03 output, so $0.11 per ticket, $22 a day, about $660 over 30 days.
- Sonnet 5.5, 30,000 of the 40,000 input tokens served from cache: $0.003 + $0.02 + $0.03 = about $0.053 per ticket, roughly $318 a month (cache writes cost extra and are left out here for simplicity).
- Haiku 5.5, no caching: 40,000 × $0.10/M + 3,000 × $0.50/M = about $0.0055 per ticket, roughly $33 a month.
- Hosting: one always-on container at $0.05 an hour is about $36 a month.
The lesson: model choice moves the bill by more than an order of magnitude, and caching roughly halves it again. The SDK applies prompt caching automatically (cost tracking docs). Test triage quality on the cheap model first and move up only for the cases it gets wrong. For a broader view of agent spend, see our breakdown of the cost of running multi-agent systems.
Two limits to plan around. The total_cost_usd field is a client-side estimate computed from a price table bundled with the SDK, not billing data; Anthropic says not to bill end users or make financial decisions from it and to use the Usage and Cost API instead (cost tracking docs). And for sizing, Anthropic suggests 1 GiB RAM, 5 GiB disk and 1 CPU per agent as a starting point, noting memory grows with session length (hosting guide).
Hosting and running the agent in production
Because every session is a subprocess with local state, pick a session pattern before you pick a cloud. The hosting guide describes four (hosting guide):
- Ephemeral: one container per task, destroyed when done. Best for invoice extraction, document translation and the triage batch above.
- Long-running: persistent containers holding many sessions. Best for an email agent or a Slack bot with continuous traffic.
- Hybrid: ephemeral containers that reload a session from a
SessionStorewhen a user returns. Without a store, shutting a container down loses the transcript. - Multi-agent container: several SDK processes collaborating in one container, each with its own working directory.
For multi-tenant services, isolation is not optional. By default the SDK reads settings and CLAUDE.md files from the filesystem, so one tenant’s context can leak into another’s. The documented fix is to pass setting_sources=[], set CLAUDE_CODE_DISABLE_AUTO_MEMORY=1, give each tenant its own CLAUDE_CONFIG_DIR and working directory, and apply per-tenant egress rules at a proxy (hosting: multi-tenant isolation).
For visibility, the SDK inherits OpenTelemetry settings from the environment, so setting CLAUDE_CODE_ENABLE_TELEMETRY=1 and the standard OTEL_* exporter variables sends spans, metrics and logs to your collector; prompt text and tool inputs are excluded by default (hosting: observability). Keep tool credentials out of the agent’s environment and inject them at an outbound proxy instead, so a prompt-injected agent cannot read your keys.
Finally, keep a human in the loop where actions are hard to undo: refunds, emails to customers, deletes. Start the agent in a mode that drafts rather than sends, and loosen it as trust builds. Our guide to human-in-the-loop for agentic AI covers the approval patterns.
A real example: what changes from demo to production
Here is how the triage agent above typically evolves at a 30-person software company, and why each change happens. The figures in this section are illustrative of the pattern, not a published case study.
Week 1, the demo. The agent reads 50 old tickets and writes a triage file. It agrees with the support lead on most tickets but marks every message containing the word “urgent” as P1. The fix is not more code: the system prompt gains three short examples of what P1 actually means, and the lookup tool now returns the customer’s plan, so “urgent” from a free-plan user stops jumping the queue.
Week 2, the first real run. The team wires the CSV lookup to the CRM API and moves the run into an ephemeral container triggered every 15 minutes. One run loops on a ticket with an attachment it cannot read and burns through its turns. max_turns and max_budget_usd stop it, the error result is logged, and the tool is changed to return a clear “attachment not readable, mark for human” message.
Week 4, scale. Volume grows, so the team runs the cheap model by default and adds a refund-review subagent on a stronger model only for refund requests. They read model_usage on each result to see spend per model, and they reconcile monthly against the Console’s usage page rather than the SDK estimate.
The pattern is consistent across clients: most improvements come from better tool results and clearer instructions, not from more agents. If you are tempted to split the work across many agents early, read single agent vs multi-agent first.
Common mistakes and how to avoid them
- Trusting
allowed_toolsto restrict the agent. It only pre-approves the tools you list. Unlisted tools still exist, and withbypassPermissionsthey are approved too; Anthropic’s docs state thatallowed_tools=["Read"]withbypassPermissions“still approves every tool, including Bash, Write, and Edit” (permissions docs). Usetoolsordisallowed_toolsto remove what the agent must not have. - Putting security checks in
canUseTool. Any call auto-approved earlier, by an allow rule or permission mode, never reaches that callback. Checks that must run on every call belong in aPreToolUsehook. - Running without turn or budget limits. There is no session timeout. A confused agent will keep trying. Set
max_turnsandmax_budget_usdon every production query. - Letting host settings leak in. A developer’s
~/.claudehooks or a project’s.mcp.jsoncan change behaviour between machines. Usesetting_sources=[]in the SDK, or--bareon the CLI, unless you mean to load them. - Double-counting cost. Parallel tool calls share one message ID, and a resumed session’s result already includes earlier spend. Deduplicate by ID and read the latest result rather than summing (cost tracking docs).
- Vague tool descriptions. Claude chooses tools from their descriptions alone. “Get data” will be misused; “Look up a customer’s plan and account owner by email address” will not.
- Skipping the matcher gap. A hook with a
Write|Editmatcher does not see a file written through Bash (hooks docs). Either remove Bash or cover it with its own rule.
Alternatives and when to use them
Claude Code and the Agent SDK are not the only way to build an agent, and for some teams they are not the best one.
- Low-code automation (n8n, Make, Zapier). Use when the steps are known in advance and the team that maintains it does not write code. You can still call Claude from a single step for the judgment part.
- Framework libraries such as LangChain, LangGraph or CrewAI. Use when you need to mix model providers or want explicit graph control over every step. You give up Claude Code’s built-in file, shell and web tools and write more glue. Our LangChain agents tutorial shows that style for comparison.
- Managed Agents. Use when you want Anthropic to host the loop and sandbox. You pay the session-hour fee and get less control of the machine, but skip containers, session storage and scaling.
- Self-hosted open agents. Use when you want a chat-first assistant in Slack or Telegram with any model. They suit personal and team assistants more than product features.
- The raw Messages API. Use for narrow, high-volume calls such as classifying 100,000 records, where the Batch API’s discount and full control matter more than built-in tools.
A useful test: if you can draw the workflow as a fixed flowchart, use low-code. If the agent must explore files, decide which tool to use next, and recover from surprises, Claude Code’s engine is a strong default.
Launch checklist
- Write the task as one sentence with a clear finish line (“every ticket has a priority and queue in out/triage.md”).
- Pick the route: CLI for internal jobs, Agent SDK for your app, Managed Agents for no infrastructure.
- Get the smallest agent running with your API key before adding tools.
- Add custom tools with specific descriptions and helpful
is_errormessages. - Remove every built-in tool the agent does not need with
toolsordisallowed_tools. - Use
dontAsk(or--permission-prompts none) for unattended runs. - Add a
PreToolUsehook for every rule that must never be broken. - Set
max_turnsandmax_budget_usdon every query. - Isolate settings:
setting_sources=[]or--bare, plus per-tenant directories if you serve customers. - Test on 20 to 50 real past cases and compare with a human’s answers before going live.
- Start on the cheapest model that passes, and route only hard cases to a stronger one.
- Export OpenTelemetry data and reconcile cost monthly against the Console, not the SDK estimate.
- Keep a human approval step for anything irreversible until error rates are known.
FAQ
Is the Claude Agent SDK the same as the Claude Code SDK?
Yes. The Claude Code SDK was renamed to the Claude Agent SDK, and Anthropic publishes a migration guide for the package and import changes. The engine is the same one that powers Claude Code. If you find older tutorials using the old package names, follow the migration guide rather than mixing versions.
Can I build an agent without writing code?
Mostly, yes. A repository with a CLAUDE.md, a few skills and a scheduled claude -p command is a working agent with no application code. You still need someone comfortable editing files and reading logs. For a purely visual builder, a low-code tool is the better fit.
Which model should my agent use?
Start with the cheapest model that handles your test cases, often Haiku or Sonnet, and measure. You can set a model per subagent, so a cheap model can do the bulk of the work while a stronger one handles the few hard cases. Check current pricing before you commit, because prices change.
Can my customers log in with their own Claude subscription?
Not without approval. Anthropic does not allow third-party developers to offer claude.ai login or rate limits in their products, including agents built on the SDK, unless previously approved. Use API key authentication, or Bedrock, Google Cloud or Microsoft Foundry credentials, instead.
Where can I find complete example agents?
Anthropic maintains example agents on GitHub, including an email assistant and a research agent, and a hosting cookbook with Docker, Modal and Kubernetes setups. Read them for structure, then strip them down to the tools your task needs.




