Your first agent with the Claude Agent SDK
AI · Aug 2026 · 18 min read
Claude Code as a library. Installing it, the two entry points, what the message stream actually contains, and the three limits you set before you let it run.
The Claude Agent SDK is Claude Code packaged as a library. You get the agent loop, context management, and the built-in tools — Read, Write, Edit, Bash, Glob, Grep, WebSearch, WebFetch — behind a function call. You supply a prompt and some options; it does the rest on your own infrastructure.
That framing matters, because it is easy to confuse with two adjacent things. This is the tutorial for the batteries-included coding agent. It is not the Messages API tool runner, and it is not Managed Agents.
three things that all sound like "agent SDK" What Package You get / you supply Tool runner anthropic The loop only. Every tool is yours; no filesystem, no sandbox. Claude Agent SDK claude-agent-sdk Loop plus built-in tools, hooks, subagents. You host it. Managed Agents REST / anthropic Anthropic runs the loop and hosts a per-session container.
Pick this one when you want a coding or filesystem agent running on hardware you control. Pick Managed Agents when you would rather not run the container at all.
Step 1: install and authenticate
pip install claude-agent-sdk
# auth resolves the same way the SDKs and CLI do, first match wins:
# ANTHROPIC_API_KEY -> ANTHROPIC_AUTH_TOKEN -> ant auth login profile
export ANTHROPIC_API_KEY=sk-ant-...
# or, no static key to manage:
ant auth login
The auth trap: An exported ANTHROPIC_API_KEY silently outranks any profile you logged into — including an empty one, which authenticates as an empty key and fails confusingly. If a profile you just created appears to be ignored, unset the variable before debugging anything else.
Step 2: the smallest thing that runs
There are two entry points and the choice is simply whether the conversation continues. Use query() for one-shot work — it is an async generator you iterate to completion.
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def main():
options = ClaudeAgentOptions(
system_prompt='You are a careful Python developer.',
allowed_tools=['Read', 'Glob', 'Grep'], # read-only for now
cwd='/path/to/project',
)
async for message in query(
prompt='Summarise what this project does, in five bullets.',
options=options,
):
print(message)
asyncio.run(main())
# note: query() takes keyword arguments only.
# query('do a thing') is a TypeError, not a shorthand.
cwd is the agent's working directory and it is the single most consequential option in that block. The built-in file tools operate relative to it, so it is also the first boundary you are drawing around what this process can touch.
Step 3: read the message stream properly
print(message) gets you started and then immediately stops being useful. The stream is a sequence of typed objects, and once you branch on them you can render progress, log tool calls, and pull out the numbers that matter.
from claude_agent_sdk import (
query, ClaudeAgentOptions,
AssistantMessage, TextBlock, ToolUseBlock, ResultMessage,
)
async for message in query(prompt=..., options=options):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(block.text, end='')
elif isinstance(block, ToolUseBlock):
print(f'\n -> {block.name}({block.input})')
elif isinstance(message, ResultMessage):
print(f'subtype {message.subtype}') # success|error|interrupted
print(f'cost ${message.total_cost_usd}')
u = message.usage or {} # tokens live in here
print(f'tokens {u.get("input_tokens")} in / {u.get("output_tokens")} out')
print(f'cache {u.get("cache_read_input_tokens")} read')
print(f'duration {message.duration_ms} ms')
print(f'session {message.session_id}')
ResultMessage arrives exactly once, at the end, and it is where the accounting lives. Log total_cost_usd and session_id from day one. The first is how you find out an agent is expensive before the invoice does; the second is the only handle you have for resuming or inspecting the run afterwards.
Tokens are nested, cost is not: total_cost_usd, duration_ms, and session_id are attributes on ResultMessage, but the token counts are keys inside the usage dict — reaching for message.input_tokens raises AttributeError. Verified against claude-agent-sdk 0.2.129.
Check subtype, not just result: subtype is success, error, or interrupted. A run that hit an error still yields a ResultMessage and still ends the loop cleanly — if you only read .result you will treat a failed run as a finished one.
Step 4: the three limits you set before letting it run
An agent loop with no ceiling is an open-ended spend commitment attached to a while loop. Three options bound it, and they bound different things.
options = ClaudeAgentOptions(
max_turns=30, # agentic turns before it stops
max_budget_usd=2.00, # hard spend ceiling for this run
permission_mode='acceptEdits',
allowed_tools=['Read', 'Edit', 'Write', 'Glob', 'Grep'],
model='claude-opus-5',
effort='high', # low | medium | high | xhigh | max
)
# a tool absent from allowed_tools is not silently allowed - it
# falls through to your permission handler, or is denied.
max_budget_usd is the one people skip and then wish they hadn't. max_turns bounds iterations, but a single turn on a large codebase is not cheap, so turns are a poor proxy for money. Set both.
Step 5: pick a permission mode deliberately
The permission mode decides what happens when the agent wants to do something you did not pre-approve. This is the security posture of the whole process expressed as one string, so it deserves more than a copy-paste.
permission modes, from cautious to reckless Mode Behaviour Use for plan Plans, does not execute Dry runs and scoping default Asks on anything unapproved Interactive local work acceptEdits Auto-approves file edits Supervised refactors dontAsk Denies rather than prompting CI, where nobody can answer bypassPermissions Approves everything Throwaway sandboxes only
On bypassPermissions: It does what the name says: every tool call, including Bash, runs unreviewed. There is a legitimate use — a disposable container you are willing to lose — and outside that, reach for dontAsk plus an explicit allowed_tools list instead. Unattended is a reason for more constraint, not less.
Step 6: keep the conversation going
query() is one-shot. When you need turns that build on each other — a chat surface, a review loop, anything where the second instruction depends on the first answer — use ClaudeSDKClient, which holds the session open.
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
async with ClaudeSDKClient(options=options) as client:
await client.query('Find the slowest test in the suite.')
async for message in client.receive_response():
handle(message)
# context is retained - 'it' resolves to the test above
await client.query('Now make it faster without changing what it asserts.')
async for message in client.receive_response():
handle(message)
await client.interrupt() # stop a runaway turn
await client.set_permission_mode('plan') # tighten mid-session
receive_response() yields until the current turn completes, which is what you want in a request handler. interrupt() is worth wiring to a cancel button early — an agent that cannot be stopped is a support ticket waiting to happen.
Step 7: resume across process restarts
Sessions outlive the process. Keep the session_id from ResultMessage and a later run can pick the thread back up, which is what makes long-lived agents practical.
from claude_agent_sdk import (
query, ClaudeAgentOptions,
list_sessions, get_session_messages, rename_session,
)
# resume a specific past session by id
options = ClaudeAgentOptions(resume=saved_session_id)
# or just continue the most recent one in this directory
options = ClaudeAgentOptions(continue_conversation=True)
# inspect without running anything (these are synchronous)
for s in list_sessions(directory='/path/to/project', limit=10):
print(s)
rename_session(saved_session_id, 'Nightly dependency audit')
Name your sessions: rename_session and tag_session cost one line each and turn an opaque list of ids into something you can actually triage a week later. Do it at the end of every run, keyed on what the run was for.
What to build first
Resist the instinct to start with an autonomous agent. The first thing worth shipping is a narrow one: read-only tools, a specific question, max_budget_usd set low, output you check by hand. That gets you the message-stream handling, the cost logging, and the session plumbing — all the parts you will need regardless — while the blast radius is still zero.
Widening it later is a matter of adding tools and relaxing the permission mode, both one-line changes. Going the other way, after an agent with Bash and bypassPermissions has done something surprising to a working tree, is a considerably worse afternoon.
References
Takeaways
The Agent SDK is the Claude Code harness as a library — built-in tools included, hosting still yours. Branch on the message stream and log total_cost_usd and session_id from the first run. Set max_turns and max_budget_usd together; turns are a poor proxy for spend. Start read-only with a low budget — widening scope later is a one-line change, unwinding damage is not.
All notes · Shehzad Aslam