The Claude Agent SDK
Anthropic's official SDK for building agents in code — the programmatic engine behind Claude Code. It sits between the raw messages loop (you write the loop) and the Claude Code CLI (fully packaged): the SDK runs the agent loop, and you supply tools, permissions, and a system prompt through an options object. Learn the real query() pattern, sessions, and subagents — with the honest caveat that the API surface is still evolving.
Learning objectives
- Say what the Claude Agent SDK is and why it sits between the raw loop and the CLI.
- Install it:
pip install claude-agent-sdkplus the Claude Code CLI and anANTHROPIC_API_KEY. - Run an agent with the high-level async
query(...)entry point. - Configure tools, a system prompt, and permissions via a
ClaudeAgentOptionsobject. - Hold a multi-turn session with
ClaudeSDKClient, and delegate to subagents. - Choose deliberately between the raw messages loop, the Agent SDK, and the Claude Code CLI.
- Name the production concerns: permissions/sandboxing, cost caps, and observability.
1 · What the Claude Agent SDK is essential
Back in Agents from scratch you wrote the loop yourself: send a message, read the tool call Claude asked for, run the tool, feed the result back, repeat until Claude stops. That loop is the beating heart of every agent — and once you've written it a few times you notice it's always the same shape. Parsing tool-use blocks, appending results, capping the turns, gating dangerous actions, remembering the conversation: plumbing you rewrite on every project.
The Claude Agent SDK is Anthropic's official library for building agents in code — it runs that loop for you. It's the claude-agent-sdk Python package (it was called the "Claude Code SDK" earlier — same lineage), and it is the programmatic engine behind the Claude Code CLI. You hand it a prompt, a set of tools, a permission policy, and a system prompt; it drives the perceive → act → observe cycle, manages the session, enforces permissions, and can spin up subagents — all the machinery you hand-rolled in ch04, but maintained by the people who build Claude.
Think of it as the layer you reach for when the CLI is too packaged (you want it embedded in your program, driven by your code) but the raw loop is too much undifferentiated plumbing (you don't want to own tool dispatch and permission gating by hand).
claude_agent_sdk.2 · Three ways to build with Claude essential
Before any code, get the map straight. There are three levels at which you can build on Claude, and the Agent SDK is the middle one. Read this left to right — each step hands more of the agent loop to Anthropic and asks less wiring of you:
This picture is the map for the whole lesson: three levels at which you can build on Claude. Read it left to right — each step hands more of the agent loop to Anthropic and asks less wiring of you.
- Left — Raw messages loop: you write the loop yourself (the ch04 way). You own tool dispatch, the turn cap, permission checks, and memory. Most control, most plumbing.
- Middle — Claude Agent SDK: the SDK runs the loop. You just declare tools, permissions, and a system prompt through an options object. This is the subject of this lesson.
- Right — Claude Code CLI: everything is packaged into a terminal program you drive by talking to it. No code at all.
- The arrows point from hand-written toward packaged. The key fact: the CLI is built on the SDK, which is built on the messages loop — same engine, three amounts of packaging.
In short: Ask yourself "who writes the loop?" — you (left), the SDK (middle), or the CLI (right). That single question places most tasks on the right rung.
On the left (ch04) you own everything: the loop, tool dispatch, the turn cap, permission checks, memory. Maximum control, maximum plumbing. In the middle — the Agent SDK — the SDK owns the loop and the machinery; you declare tools, permissions, and a system prompt through an options object and let it run. On the right (Claude Code) everything is packaged into a terminal program you drive by talking to it — no code at all. The SDK is what the CLI is built on: same engine, exposed as a library so you can embed it in your own application.
| Level | Who writes the loop? | You provide | Reach for it when |
|---|---|---|---|
| Raw messages loop (ch04) | You do | The whole loop, tool dispatch, gates, memory | You need total control or you're learning how agents work. |
| Claude Agent SDK | The SDK does | Tools, permissions, system prompt via options | You want an agent inside your program without owning the plumbing. |
| Claude Code CLI (cl3) | The CLI does | Just your prompts, in a terminal | You want a ready-made coding agent and don't need to embed it. |
3 · Install & requirements essential
The SDK is a thin Python layer that drives the Claude Code CLI under the hood, which in turn talks to the Anthropic API. So you need three things, not one:
What to install
- The Python package:
pip install claude-agent-sdk. - The Claude Code CLI on your PATH — the SDK launches it as a subprocess. Install it per the Claude Code docs (typically via
npm install -g @anthropic-ai/claude-code, but check the current instructions). - Credentials: an
ANTHROPIC_API_KEYin your environment (or another auth method the CLI supports). The key is read from the environment — it never appears in your code.
setup.sh# needs: pip install claude-agent-sdk + Claude Code CLI + ANTHROPIC_API_KEY;
# API names evolve — verify in docs (claude-agent-sdk Python reference).
# 1. the SDK (Python)
pip install claude-agent-sdk
# 2. the Claude Code CLI it drives (verify the current install command in the docs)
npm install -g @anthropic-ai/claude-code
# 3. your key, in the environment (never in code)
export ANTHROPIC_API_KEY="sk-ant-..."
pip install claude-agent-sdk alone is not enough. The SDK shells out to the Claude Code CLI, so if the CLI isn't installed and on your PATH the SDK will fail at runtime even though the import succeeds. Verify both.4 · Recipe 1 — the high-level query() essential
The simplest way in is the async query() function. You give it a prompt, and it returns an async stream of messages — the SDK runs the whole agent loop internally and yields each message (Claude's text, tool calls, tool results, a final result) as it happens. This is the same loop you wrote by hand in ch04, collapsed to one call.
recipe1_query.py# needs: pip install claude-agent-sdk + Claude Code CLI + ANTHROPIC_API_KEY;
# API names evolve — verify in docs (claude-agent-sdk Python reference).
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def main():
# query() is async and returns an async iterator of messages.
# The SDK runs the agent loop for you and yields messages as they arrive.
async for message in query(
prompt="List the Python files in this folder and summarize what each does.",
options=ClaudeAgentOptions(
system_prompt="You are a concise coding assistant.",
allowed_tools=["Read", "Glob", "Grep"], # read-only: no edits, no shell
),
):
# Message/block class names evolve — verify in the docs. The final
# result typically arrives as a message carrying a `.result` string.
if hasattr(message, "result"):
print(message.result)
asyncio.run(main())
This is the smallest possible agent with the SDK. In ch04 you wrote a whole loop to do this; here one call, query(...), runs the entire perceive → act → observe cycle and streams the messages back to you.
query(...)is async and returns an async iterator, which is why we writeasync for message in query(...)and run it insideasyncio.run(main()). Each loop pass hands you one message the agent produced.prompt=is the task in plain English. The SDK figures out which tools to call, calls them, feeds the results back to Claude, and repeats — all inside that one call.options=ClaudeAgentOptions(...)is where you configure the agent. Here a shortsystem_promptand a read-onlyallowed_toolslist (Read,Glob,Grep) so it can look but not edit or run shell.- The
if hasattr(message, "result"):check picks out the final message (the one carrying the finished answer) and prints it. The comment is honest: exact message class names evolve, so verify them in the docs.
What the output means: The agent lists and summarizes the Python files, and the final result string prints. (This block needs the SDK + CLI + an API key — it is not expected to run in the course sandbox.)
Try this: Compare this to ch04's hand-written loop: everything you wrote by hand — the send, the tool dispatch, the loop, the stop condition — is now hidden behind query().
client.messages.create() which hands back one response, query() yields messages as the agent works — text, tool calls, tool results, and finally a result message. You loop over it. That streaming shape is why the whole multi-step loop can hide behind a single call.5 · Recipe 2 — tools, prompt & permissions via options intermediate
Everything you configure about the agent goes on the options object — ClaudeAgentOptions. This is where the SDK earns its keep: instead of hand-writing a tool dispatcher and a permission gate (ch04, Lab 4.4), you declare them. Custom tools are defined with the @tool decorator and bundled into an in-process MCP server with create_sdk_mcp_server(...); the SDK handles the dispatch.
recipe2_tools.py# needs: pip install claude-agent-sdk + Claude Code CLI + ANTHROPIC_API_KEY;
# API names evolve — verify in docs (claude-agent-sdk Python reference).
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, tool, create_sdk_mcp_server
# @tool(name, description, input_schema) — the schema shape here is {arg_name: type}.
# Verify the exact input_schema form + return shape in the docs; names evolve.
@tool("get_weather", "Get the current weather for a city", {"city": str})
async def get_weather(args):
city = args["city"]
# ... your real implementation (call a weather API, read a DB, etc.) ...
return {"content": [{"type": "text", "text": f"{city}: 22C and sunny"}]}
# Bundle your tools into an in-process MCP server the SDK can call.
weather = create_sdk_mcp_server(name="weather", version="1.0.0", tools=[get_weather])
options = ClaudeAgentOptions(
system_prompt="You are a travel assistant. Use tools before you guess.",
mcp_servers={"weather": weather},
# tool names are namespaced mcp__<server>__<tool> — verify the prefix in the docs
allowed_tools=["mcp__weather__get_weather"],
permission_mode="default", # default | acceptEdits | plan | dontAsk | bypassPermissions | auto
)
async def main():
async for message in query(prompt="What should I pack for Lisbon?", options=options):
if hasattr(message, "result"):
print(message.result)
asyncio.run(main())
This shows how you give the agent a custom tool of your own and wire up its permissions — all through the options object, instead of the hand-written tool dispatcher you built in ch04.
@tool("get_weather", ...)turns a normal async Python function into a tool the agent can call. The third argument is the input schema (here{"city": str}); the function returns a content dict. Names and the exact schema shape evolve — verify in the docs.create_sdk_mcp_server(...)bundles your tool(s) into a small in-process server the SDK knows how to call. You don't run a separate process — it lives inside your program.- On
ClaudeAgentOptions:mcp_servers=registers that server, andallowed_tools=["mcp__weather__get_weather"]is the allow-list — note themcp__<server>__<tool>naming so the agent may use it without a prompt. permission_mode="default"keeps the safe behavior (gate risky actions). The table under this block lists the other modes —bypassPermissionsis the dangerous one.
What the output means: The agent calls your get_weather tool before answering, so its packing advice is grounded in the tool's result rather than guessed.
Try this: Change allowed_tools to an empty list and the agent can no longer call your tool — that list is exactly how you keep an agent narrowly scoped.
| permission_mode | What it does |
|---|---|
default | Standard behavior — prompts (or applies your rules) before risky tools. |
acceptEdits | Auto-accepts file edits without prompting. |
plan | Planning mode — the agent explores and proposes but doesn't act. |
dontAsk | Denies anything not pre-approved instead of prompting. |
bypassPermissions | Skips permission checks. Dangerous — sandbox only. |
Read, Write, Edit, Bash, Glob, Grep, Agent, …) plus any custom tools you register. allowed_tools is the set the agent may use without a prompt — the primary lever for keeping an agent read-only or narrowly scoped. Exact built-in names evolve; verify in the docs.6 · Recipe 3 — sessions & multi-turn intermediate
query() is one-shot: each call is a fresh conversation. For a back-and-forth session that remembers — the multi-turn memory you built by hand in ch04's Lab 4.5 — use ClaudeSDKClient. You open it once, send turns with .query(...), and read each reply by iterating .receive_response(); the client keeps the history for you.
recipe3_session.py# needs: pip install claude-agent-sdk + Claude Code CLI + ANTHROPIC_API_KEY;
# API names evolve — verify in docs (claude-agent-sdk Python reference).
import asyncio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
async def main():
options = ClaudeAgentOptions(system_prompt="You are a helpful research assistant.")
# The client keeps the conversation alive across turns — no manual history list.
async with ClaudeSDKClient(options=options) as client:
await client.query("What is the capital of Portugal?")
async for message in client.receive_response():
# reading the text off a message: block/class names evolve — verify in docs
print(message)
# follow-up in the SAME session; "that city" resolves because state is kept
await client.query("What is the population of that city?")
async for message in client.receive_response():
print(message)
asyncio.run(main())
query() forgets everything between calls. When you need a conversation that remembers — the multi-turn memory you hand-built in ch04's Lab 4.5 — you switch to ClaudeSDKClient, which holds the session for you.
async with ClaudeSDKClient(options=options) as client:opens a live session and closes it cleanly at the end of the block. One client = one ongoing conversation.await client.query("...")sends a turn;async for message in client.receive_response():reads the reply for that turn. You do this once per turn.- The second question — "the population of that city" — works because the client kept the history. You never rebuilt a messages list by hand; the session state resolves "that city" to Lisbon for you.
What the output means: Two answers print in order: the capital of Portugal, then its population — the follow-up resolving correctly proves the session remembered the first turn.
Try this: Rule of thumb: query() for one-off tasks, ClaudeSDKClient when the conversation must continue across turns (or you need interrupts).
query() for a one-off task (new session each call). Use ClaudeSDKClient when you need a continuing conversation, interrupts, or to swap the permission mode mid-run. Same engine — one is stateless-per-call, the other holds a live session.7 · Recipe 4 — subagents advanced
A single agent's context fills up fast when it reads dozens of files. Subagents fix that: each is a separate agent instance with its own fresh context, its own system prompt, and its own restricted tool set. The main agent delegates a focused subtask, the subagent does the work in isolation, and only its final message comes back — the intermediate noise never pollutes the parent. Define them programmatically with AgentDefinition on the agents option; Claude invokes them through the built-in Agent tool.
recipe4_subagents.py# needs: pip install claude-agent-sdk + Claude Code CLI + ANTHROPIC_API_KEY;
# API names evolve — verify in docs (claude-agent-sdk Python reference).
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
async def main():
async for message in query(
prompt="Review the auth module for security issues.",
options=ClaudeAgentOptions(
# the parent needs the Agent tool to be allowed to delegate
allowed_tools=["Read", "Grep", "Glob", "Agent"],
agents={
"code-reviewer": AgentDefinition(
description="Security & quality review specialist. Use for code reviews.",
prompt="You are a code reviewer. Flag security and correctness issues, concisely.",
tools=["Read", "Grep", "Glob"], # read-only: this subagent can't edit
model="sonnet", # optional per-subagent model override
),
},
),
):
if hasattr(message, "result"):
print(message.result)
asyncio.run(main())
A subagent is a separate agent instance the main agent can hand a focused job to. It runs in its own fresh context with its own tools, and only its final answer comes back — so its file reading never clutters the main conversation.
allowed_tools=[..., "Agent"]— the parent must be allowed the built-inAgenttool, because that's how it delegates to a subagent.agents={"code-reviewer": AgentDefinition(...)}defines the subagent.descriptiontells Claude when to use it;promptis the subagent's own system prompt (its expertise).tools=["Read", "Grep", "Glob"]restricts the subagent to read-only — it physically cannot edit files.model="sonnet"optionally picks a cheaper/faster model just for this subagent.- Claude decides on its own to call the
code-reviewerbased on itsdescription, runs it in isolation, and folds its summary back into the main answer.
What the output means: The review comes back as one concise summary; the dozens of file reads the subagent did to produce it stayed inside the subagent and never filled the parent's context.
Try this: This is the same delegation pattern this course's own Explore agent uses — a read-only helper that fans out, reads a lot, and returns just the conclusion.
8 · Recipe 5 — decide: raw loop vs SDK vs CLI (runs offline) advanced
The most valuable skill here isn't SDK syntax — it's picking the right level for the job. The helper below is pure stdlib and runs with a plain python file.py (no API key, no network, no install). Answer a few yes/no questions about your situation and it recommends the raw loop, the Agent SDK, or the CLI, with a reason.
decide.pydef choose_build_level(*, embed_in_app, need_full_control_of_loop,
terminal_is_fine, learning_how_agents_work):
"""Recommend raw messages loop vs Claude Agent SDK vs Claude Code CLI.
All inputs are booleans describing your situation."""
if learning_how_agents_work or need_full_control_of_loop:
pick = "raw messages loop"
why = "you want to own the loop end-to-end (control, or learning the mechanics)."
elif terminal_is_fine and not embed_in_app:
pick = "Claude Code CLI"
why = "a packaged terminal agent is enough; no need to embed it in code."
elif embed_in_app:
pick = "Claude Agent SDK"
why = "you want the agent loop inside your program without hand-writing the plumbing."
else:
pick = "Claude Agent SDK"
why = "the safe default: managed loop, your tools and permissions."
return {"pick": pick, "why": why}
scenarios = {
"Embed an agent in our web backend": dict(
embed_in_app=True, need_full_control_of_loop=False,
terminal_is_fine=False, learning_how_agents_work=False),
"Quick coding help in my terminal": dict(
embed_in_app=False, need_full_control_of_loop=False,
terminal_is_fine=True, learning_how_agents_work=False),
"Teach myself how agent loops work": dict(
embed_in_app=False, need_full_control_of_loop=False,
terminal_is_fine=False, learning_how_agents_work=True),
}
for name, situation in scenarios.items():
r = choose_build_level(**situation)
print(f"{name}\n -> {r['pick']} ({r['why']})")
Embed an agent in our web backend
-> Claude Agent SDK (you want the agent loop inside your program without hand-writing the plumbing.)
Quick coding help in my terminal
-> Claude Code CLI (a packaged terminal agent is enough; no need to embed it in code.)
Teach myself how agent loops work
-> raw messages loop (you want to own the loop end-to-end (control, or learning the mechanics).)
This one runs offline — pure stdlib, no API key, no install. It turns the "which level?" decision into a function so you can reason about it deliberately instead of by gut.
choose_build_level(...)takes four yes/no facts about your situation (keyword-only, so each call reads clearly) and returns apickplus thewhy.- The
if / elif / elseladder encodes the priorities: wanting control or learning sends you to the raw loop first; a terminal being enough sends you to the CLI; otherwise embedding in an app lands on the Agent SDK, which is also the safe default. - The
scenariosdict runs three realistic situations through the function and prints each recommendation with its reason — so you can see the rule applied, not just stated.
What the output means: Three lines: "embed in our backend" → Agent SDK; "quick terminal help" → CLI; "teach myself" → raw loop. Exactly the mapping the lesson argues for.
Try this: Add your own scenario to the scenarios dict with your real answers and run it — the recommendation for your actual project falls out.
9 · Production concerns professional
Letting an SDK drive a loop that can read files, run shell commands, and spend money is powerful and risky in equal measure. Three concerns turn a demo into something you'd run for real:
Production checklist
- Permissions & sandboxing. Keep
allowed_toolsas small as the task allows, prefer read-only tools, and usepermission_modedeliberately — neverbypassPermissionsoutside a sandbox. For fine-grained control, pass acan_use_toolcallback that inspects each call and returns allow/deny (verify the exact result types in the docs). Run untrusted work in a container or throwaway workspace. - Cost. An agent loop makes many API calls, and subagents multiply that into a tree. Cap it: set a spend limit (
max_budget_usd) and bound subagent depth/concurrency (via the SDK's env options). Prefer a cheap model for routine subagent work. - Observability. The message stream is your trace — log every tool call, tool result, and the final result/usage. You cannot debug or price an agent you can't see. Persist the stream (or key events) so a failed run is diagnosable after the fact.
permission_mode="bypassPermissions" lets the agent run any allowed tool with no gate — including Bash and file writes. It's fine in a disposable sandbox and genuinely dangerous anywhere near real data or credentials. Default to default, narrow allowed_tools, and reach for a can_use_tool callback when you need real policy.10 · Tech-lead — where the SDK fits tech-lead
A lead's job is to place each build on the right rung and keep the team off the wrong ones. The Agent SDK is the right default for "an agent embedded in our product": it gives you the managed loop, session handling, permissions, and subagents without a bespoke framework you'd have to maintain — and because it's the same engine as the CLI, behavior you validate in the terminal transfers to production.
Steer by these lines. Reach down to the raw loop only when you need control the SDK doesn't expose, or you're deliberately teaching the mechanics — accept that you now own the plumbing forever. Reach up to the CLI for developer-facing terminal work where embedding buys nothing. Sit on the SDK for everything in between. And treat the honest caveat as a standing rule: this surface is young and moving — pin your claude-agent-sdk version, read the changelog before upgrading, and verify class and option names against the current docs rather than trusting a snippet (including these).
Exercise AP7.1 — Three levels, one task
Context: The same task can be built three ways with wildly different effort and control. Reasoning through one concrete job in all three shows you where each build level actually pays off.
Your task: Take one concrete task (e.g. summarize every README in a repo) and describe three ways to do it: (a) the raw messages loop, listing the plumbing you'd hand-write; (b) the Agent SDK with query() and a read-only allowed_tools; (c) the Claude Code CLI. Then use the decision helper to confirm what you'd ship and justify it.
Requirements:
- For the raw loop, name the plumbing you own: the tool-use loop, tool dispatch, message accumulation, error handling
- For the SDK, show
query()plus a read-onlyallowed_toolsand note what the SDK runs for you - For the CLI, describe the zero-code path and its trade-off in control
- Feed your task's traits into the build-level decision helper and report its answer
- Justify the shipped choice in one or two sentences tied to this task
💡 Hint: Frame the comparison as control-vs-convenience: the raw loop maximizes control, the CLI maximizes convenience, and the SDK is the middle you usually want.
Exercise AP7.2 — Lock it down
Context: A code-reading assistant that can write files or run shell is a liability. The SDK gives you several independent guardrails — allowlists, permission modes, and a tool-use callback — and real safety comes from combining them.
Your task: Sketch a ClaudeAgentOptions for an agent that may read and search a codebase but must never write files or run shell commands. Decide the allowed_tools, the permission_mode, where a can_use_tool callback adds value, how you'd cap cost, and what you'd log.
Requirements:
- List an
allowed_toolsset that is read/search only — no write or shell tools - Choose a
permission_modeand say why it fits a read-only agent - Describe what a
can_use_toolcallback would deny at runtime as a second line of defense - State a concrete cost cap (e.g. a token or turn budget) and how you'd enforce it
- Name what you'd log for observability (tool calls, denials, token usage)
💡 Hint: Treat the allowlist, the permission mode, and the callback as layers: the allowlist removes the tool, the mode gates it, and the callback is your final programmatic veto.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: The Claude Agent SDK's query() is the fastest way off the ground: one async call runs the whole agent loop for you and streams messages back. It is how most SDK apps start before they need session state.
Your task: Write a single one-shot query() call that asks Claude a question and prints the text of every message it yields.
Requirements:
- Import
queryfromclaude_agent_sdk query()is async — drive it withasync forinside a coroutine run byasyncio.run- Iterate each message's content blocks and print only the
textblocks - Guard for messages that carry no content instead of assuming a shape
- Note in a comment that this needs an API key to actually run
💡 Hint: Messages stream in as an async iterator; loop their content and keep just the blocks whose type is text.
Show solution
query() is async and returns an async iterator of messages. Await it inside asyncio.run.
import asyncio
from claude_agent_sdk import query
async def main():
async for message in query(prompt="List three uses for a hash map."):
# messages stream in; print any text blocks
for block in getattr(message, "content", []) or []:
if getattr(block, "type", None) == "text":
print(block.text)
asyncio.run(main())
Context: Handing an agent your whole toolbox is how accidents happen. ClaudeAgentOptions is where you pin the agent's persona and fence it into a safe, read-only set of tools before it ever runs.
Your task: Call query() with a ClaudeAgentOptions that sets a system_prompt and limits allowed_tools to read-only file access.
Requirements:
- Build a
ClaudeAgentOptionsand pass it as theoptions=argument toquery() system_promptgives the agent a clear, cautious roleallowed_toolslists only read-oriented tools (e.g.Read,Grep) — nothing that writes- Set an explicit
permission_moderather than relying on a default - Print the model's text output as before
💡 Hint: The persona, the tool allowlist, and the permission mode all live on the same ClaudeAgentOptions object — construct it once and pass it in.
Show solution
ClaudeAgentOptions carries the system prompt, the tool allowlist, and the permission mode.
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
opts = ClaudeAgentOptions(
system_prompt="You are a careful code reviewer. Never edit files.",
allowed_tools=["Read", "Grep"], # read-only
permission_mode="default",
)
async def main():
async for m in query(prompt="Summarize what src/ contains.", options=opts):
for b in getattr(m, "content", []) or []:
if getattr(b, "type", None) == "text":
print(b.text)
asyncio.run(main())
Context: Agents get useful when they can call your code. The SDK lets you define a tool in-process with the @tool decorator and serve it over an in-memory MCP server — no separate process, no network.
Your task: Define a @tool named add that sums two numbers, wrap it in an in-process server with create_sdk_mcp_server, and expose it through ClaudeAgentOptions.
Requirements:
- The
@tooldecorator takes a name, a description, and an input schema - The tool returns a content-block dict (a
contentlist of typed blocks), not a bare value - Register it with
create_sdk_mcp_serverand list it undermcp_serversin the options - Reference the tool in
allowed_toolsusing themcp__<server>__<tool>naming - Show the tool's contract offline by calling it directly and printing the returned block
💡 Hint: The tool body is just an async function; you can invoke it yourself with a plain args dict to verify the content-block shape without any API key.
Show solution
The @tool decorator takes name, description, and an input schema; tools return a content-block dict. Custom tools are referenced as mcp__<server>__<tool>.
from claude_agent_sdk import tool, create_sdk_mcp_server, ClaudeAgentOptions
@tool("add", "Add two numbers", {"a": float, "b": float})
async def add(args):
total = args["a"] + args["b"]
return {"content": [{"type": "text", "text": str(total)}]}
server = create_sdk_mcp_server(name="calc", version="1.0.0", tools=[add])
opts = ClaudeAgentOptions(
mcp_servers={"calc": server},
allowed_tools=["mcp__calc__add"],
)
# offline check of the tool contract:
import asyncio
print(asyncio.run(add({"a": 2, "b": 3}))) # {'content': [{'type':'text','text':'5.0'}]}
Context: query() forgets everything between calls, which breaks any real conversation. ClaudeSDKClient holds a live session so turn two can build on turn one.
Your task: Hold a two-turn conversation where the second turn remembers a fact from the first, using ClaudeSDKClient as an async context manager.
Requirements:
- Enter the client with
async with ClaudeSDKClient(...)so the session spans both turns - Send each turn with
.query()and drain it with.receive_response() - Turn one states a fact; turn two asks about it in the same session
- Fully consume the first response before sending the second turn
- Print the second answer to show the fact was retained
💡 Hint: The point is that both .query() calls happen inside one async with block — the same client instance is the memory.
Show solution
query() forgets between calls; ClaudeSDKClient keeps session memory. Use async with, then .query() and .receive_response().
import asyncio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
async def main():
async with ClaudeSDKClient(options=ClaudeAgentOptions()) as client:
await client.query("My name is Ada. Remember it.")
async for _ in client.receive_response():
pass
await client.query("What is my name?") # same session -> remembers
async for m in client.receive_response():
for b in getattr(m, "content", []) or []:
if getattr(b, "type", None) == "text":
print(b.text)
asyncio.run(main())
Context: Big agents stay sane by delegating: a focused subagent gets its own prompt, its own restricted tools, and often a cheaper model, keeping the main agent's context clean.
Your task: Define an AgentDefinition for a read-only reviewer subagent and register it so the main agent can delegate code review to it.
Requirements:
- Create an
AgentDefinitionwith its owndescriptionandprompt - Restrict its
toolsto read-only access (e.g.Read,Grep) - Override its
modelto a cheaper tier (e.g.sonnet) - Register it under the
agentsdict ofClaudeAgentOptionskeyed by name - Have the main
system_promptinstruct delegation to that subagent - Read the registered subagent's model back out to prove it took
💡 Hint: A subagent is an isolated instance — everything that scopes it (prompt, tools, model) lives on its AgentDefinition, not on the parent.
Show solution
Subagents are isolated instances with their own context, tools, and model override. Register them in the agents dict.
from claude_agent_sdk import ClaudeAgentOptions, AgentDefinition
reviewer = AgentDefinition(
description="Reviews diffs for bugs; read-only.",
prompt="You are a strict reviewer. Report issues; never edit.",
tools=["Read", "Grep"],
model="sonnet", # per-agent model override
)
opts = ClaudeAgentOptions(
agents={"reviewer": reviewer},
system_prompt="Delegate code review to the reviewer subagent.",
)
print(opts.agents["reviewer"].model) # sonnet
Context: Platform teams have to pick one way for every internal app to call Claude. The real trade is control vs convenience: a raw messages loop, the Agent SDK, or the zero-code Claude Code CLI.
Your task: Encode the decision as choose_build_level(need_custom_tools, want_managed_loop, want_zero_code, need_fine_control) returning 'raw-loop', 'agent-sdk', or 'claude-code-cli', then justify one recommendation.
Requirements:
- Zero-code with no need for fine control routes to the
claude-code-cli - A demand for fine control without a managed loop routes to the
raw-loop - Custom tools plus a managed loop routes to the
agent-sdk - The function returns exactly one of the three literal strings
- Runs fully offline — it is pure decision logic, no API call
- Walk through one concrete team's inputs and justify the recommendation in a comment
💡 Hint: Order the branches so the most decisive signals (zero-code, fine-control) are tested first and the SDK falls out as the balanced default.
Show solution
The three build levels trade control for convenience. This mirrors the lesson's decision helper.
def choose_build_level(need_custom_tools, want_managed_loop,
want_zero_code, need_fine_control):
if want_zero_code and not need_fine_control:
return "claude-code-cli" # fully packaged, no code
if need_fine_control and not want_managed_loop:
return "raw-loop" # you own the perceive-act-observe loop
return "agent-sdk" # SDK runs the loop, you supply tools
# A team wanting custom tools + a managed loop, some control:
rec = choose_build_level(need_custom_tools=True, want_managed_loop=True,
want_zero_code=False, need_fine_control=False)
print(rec) # agent-sdk
# Justification: custom tools + managed loop = SDK; CLI is too opaque for
# an embeddable library, a raw loop is needless maintenance here.
✓ Checkpoint — you can move on when you can…
- Explain what the Claude Agent SDK is and why it sits between the raw loop and the CLI.
- Install it:
pip install claude-agent-sdk+ the Claude Code CLI + an API key. - Run an agent with the async
query()entry point and read its message stream. - Configure tools, a system prompt, and
permission_modeviaClaudeAgentOptions. - Hold a multi-turn session with
ClaudeSDKClientand delegate to a subagent. - Choose the right level (raw loop / SDK / CLI) and name the permissions, cost, and observability concerns.
Knowledge check check yourself
Where does the Claude Agent SDK sit relative to the raw messages loop and the Claude Code CLI, and when should you reach for the SDK?
Show answer
ClaudeAgentOptions; the raw loop means you write everything, and the CLI is a fully packaged terminal program built on the SDK. Reach for the SDK when embedding an agent in your own product and you want a managed loop plus permissions without owning the plumbing.Why is allowed_tools the primary access-control lever in the SDK, and what makes permission_mode="bypassPermissions" dangerous?
Show answer
allowed_tools is an explicit allow-list of exactly which tools the agent may call, so keeping it minimal and read-only bounds the blast radius. bypassPermissions skips all permission checks, so it removes every gate on risky actions and should only be used inside a sandbox.