AI EngineeringZero to ProductionHome·About·Contact
Anthropic API in Practice · Part 7

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.

⏱️ ~1.5 hours🧪 5 recipes🎯 Beginner→Tech-lead

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-sdk plus the Claude Code CLI and an ANTHROPIC_API_KEY.
  • Run an agent with the high-level async query(...) entry point.
  • Configure tools, a system prompt, and permissions via a ClaudeAgentOptions object.
  • 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).

Formerly "Claude Code SDK"The package was renamed from the Claude Code SDK to the Claude Agent SDK to signal it's for building any agent, not only coding ones. You'll still see the old name in older posts. The import today is 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:

Raw messages loop you write the loop Claude Agent SDK SDK runs the loop Claude Code CLI fully packaged
🗺️ How to read this diagram

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.

LevelWho writes the loop?You provideReach for it when
Raw messages loop (ch04)You doThe whole loop, tool dispatch, gates, memoryYou need total control or you're learning how agents work.
Claude Agent SDKThe SDK doesTools, permissions, system prompt via optionsYou want an agent inside your program without owning the plumbing.
Claude Code CLI (cl3)The CLI doesJust your prompts, in a terminalYou 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

  1. The Python package: pip install claude-agent-sdk.
  2. 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).
  3. Credentials: an ANTHROPIC_API_KEY in your environment (or another auth method the CLI supports). The key is read from the environment — it never appears in your code.
Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Shell · one-time setup ▶ needs SDK + API key
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-..."
The CLI is a hard dependencyThis trips people up: 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.

Python · claude-agent-sdk — one-shot query() ▶ needs SDK + API key
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())
▶ How this works

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.

  1. query(...) is async and returns an async iterator, which is why we write async for message in query(...) and run it inside asyncio.run(main()). Each loop pass hands you one message the agent produced.
  2. 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.
  3. options=ClaudeAgentOptions(...) is where you configure the agent. Here a short system_prompt and a read-only allowed_tools list (Read, Glob, Grep) so it can look but not edit or run shell.
  4. 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().

It's a stream, not a return valueUnlike 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 objectClaudeAgentOptions. 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.

Python · claude-agent-sdk — a custom tool + options ▶ needs SDK + API key
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())
▶ How this works

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.

  1. @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.
  2. 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.
  3. On ClaudeAgentOptions: mcp_servers= registers that server, and allowed_tools=["mcp__weather__get_weather"] is the allow-list — note the mcp__<server>__<tool> naming so the agent may use it without a prompt.
  4. permission_mode="default" keeps the safe behavior (gate risky actions). The table under this block lists the other modes — bypassPermissions is 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_modeWhat it does
defaultStandard behavior — prompts (or applies your rules) before risky tools.
acceptEditsAuto-accepts file edits without prompting.
planPlanning mode — the agent explores and proposes but doesn't act.
dontAskDenies anything not pre-approved instead of prompting.
bypassPermissionsSkips permission checks. Dangerous — sandbox only.
allowed_tools is your allow-listThe SDK ships with built-in tools (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.

Python · claude-agent-sdk — a multi-turn session ▶ needs SDK + API key
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())
▶ How this works

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.

  1. 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.
  2. 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.
  3. 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() vs ClaudeSDKClientUse 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.

Python · claude-agent-sdk — programmatic subagents ▶ needs SDK + API key
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())
▶ How this works

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.

  1. allowed_tools=[..., "Agent"] — the parent must be allowed the built-in Agent tool, because that's how it delegates to a subagent.
  2. agents={"code-reviewer": AgentDefinition(...)} defines the subagent. description tells Claude when to use it; prompt is the subagent's own system prompt (its expertise).
  3. 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.
  4. Claude decides on its own to call the code-reviewer based on its description, 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.

Why subagents beat one giant promptThree wins: context isolation (the subagent's file reads don't fill the parent's window), parallelism (independent subagents run at once — a review can fan out style, security, and tests together), and tool restriction (a read-only reviewer physically can't edit). It's the same delegation pattern this very course's Explore agent uses.

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.

Python · pick your build level (runs offline)
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).)
▶ How this works

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.

  1. choose_build_level(...) takes four yes/no facts about your situation (keyword-only, so each call reads clearly) and returns a pick plus the why.
  2. The if / elif / else ladder 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.
  3. The scenarios dict 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.

The one-line testIn a terminal, for me? → CLI. In my program, but I don't want to own the loop? → Agent SDK. I need total control or I'm learning? → raw loop. Most production "agent inside our app" work lands squarely on the SDK.

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

  1. Permissions & sandboxing. Keep allowed_tools as small as the task allows, prefer read-only tools, and use permission_mode deliberately — never bypassPermissions outside a sandbox. For fine-grained control, pass a can_use_tool callback 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.
  2. 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.
  3. 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.
bypassPermissions is a loaded gunpermission_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).

Pin the version; verify the namesThe Agent SDK's API has already been renamed once (Claude Code SDK → Claude Agent SDK) and its classes and options keep evolving. Every SDK block in this lesson is labeled and carries a "verify in docs" comment for exactly this reason. In production: pin the version, and let the official Python reference — not memory — be the source of truth for names.

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-only allowed_tools and 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_tools set that is read/search only — no write or shell tools
  • Choose a permission_mode and say why it fits a read-only agent
  • Describe what a can_use_tool callback 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.

Exercise 1 · One-shot query() callBeginner

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 query from claude_agent_sdk
  • query() is async — drive it with async for inside a coroutine run by asyncio.run
  • Iterate each message's content blocks and print only the text blocks
  • 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())
Exercise 2 · Set a system prompt and restrict toolsIntermediate

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 ClaudeAgentOptions and pass it as the options= argument to query()
  • system_prompt gives the agent a clear, cautious role
  • allowed_tools lists only read-oriented tools (e.g. Read, Grep) — nothing that writes
  • Set an explicit permission_mode rather 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())
Exercise 3 · Register a custom in-process toolAdvanced

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 @tool decorator takes a name, a description, and an input schema
  • The tool returns a content-block dict (a content list of typed blocks), not a bare value
  • Register it with create_sdk_mcp_server and list it under mcp_servers in the options
  • Reference the tool in allowed_tools using the mcp__<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'}]}
Exercise 4 · Multi-turn session with ClaudeSDKClientExpert

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())
Exercise 5 · A specialized subagentProfessional

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 AgentDefinition with its own description and prompt
  • Restrict its tools to read-only access (e.g. Read, Grep)
  • Override its model to a cheaper tier (e.g. sonnet)
  • Register it under the agents dict of ClaudeAgentOptions keyed by name
  • Have the main system_prompt instruct 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
Exercise 6 · Choose raw loop vs SDK vs CLI for a real projectIndustry scenario

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_mode via ClaudeAgentOptions.
  • Hold a multi-turn session with ClaudeSDKClient and delegate to a subagent.
  • Choose the right level (raw loop / SDK / CLI) and name the permissions, cost, and observability concerns.

Knowledge check check yourself

✓ Knowledge check

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
The SDK runs the agent loop for you while you supply tools, permissions, and a system prompt via 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.
✓ Knowledge check

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.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in