Skip to content

C1 · lesson 2 · 10 min read

Agent SDK basics

Embed Claude Code's agent loop in your own Python or TypeScript program, with limits, permissions and a result you can check.

Ընտրեք տեքստը՝ հայերեն թարգմանությունը տեսնելու համար։Highlight text to see Armenian. Machine-assisted translation; English is the source of truth.

The Agent SDK is Claude Code as a library. It gives you the same built-in tools, agent loop, permissions, hooks, subagents and context management, driven from Python or TypeScript. The packages bundle a native Claude Code binary, so most installs need nothing else.

You want to Use
Run Claude Code’s agent inside your own process Agent SDK
Call the API and write the tool loop yourself Client SDK (anthropic), or its tool runner
Let Anthropic host the agent and its sandbox Managed Agents
Drive the agent from another language, such as Go claude -p --output-format json as a subprocess

Install and first run

pip install claude-agent-sdk                 # Python 3.10+
npm install @anthropic-ai/claude-agent-sdk   # Node 18.14+

The SDK reads ANTHROPIC_API_KEY from the environment (it doesn’t load .env files). Bedrock, Vertex and Foundry are selected with environment flags such as CLAUDE_CODE_USE_BEDROCK=1.

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage


async def main():
    options = ClaudeAgentOptions(
        allowed_tools=["Read", "Grep", "Glob"],  # auto-approved
        disallowed_tools=["WebFetch"],           # removed entirely
        permission_mode="dontAsk",               # anything else: denied, no prompt
        system_prompt="You triage CI failures. Quote the failing line.",
        max_turns=10,
        max_budget_usd=0.50,
    )
    try:
        async for msg in query(prompt="Why did logs/build.log fail?", options=options):
            if isinstance(msg, ResultMessage):
                print(msg.subtype, msg.num_turns, msg.total_cost_usd)
                if msg.subtype == "success":
                    print(msg.result)
    except Exception as err:  # query() raises after an error result
        print(f"agent stopped: {err}")


asyncio.run(main())

query() is an async iterator. You consume messages while Claude works, and the loop ends when the task is done or a limit is hit. In TypeScript the options are camelCase (allowedTools, permissionMode, maxTurns, maxBudgetUsd), and you check message.type === "result" instead of isinstance.

Options you’ll use most

Option (Python) What it does
allowed_tools Auto-approves the listed tools. It does not hide the others
disallowed_tools Removes a tool ("Bash") or denies a pattern ("Bash(rm *)") in every mode
permission_mode default, acceptEdits, plan, dontAsk, auto, bypassPermissions
can_use_tool Your callback for calls nothing else decided (lesson 5)
max_turns / max_budget_usd Loop guards. Both have no limit by default
system_prompt Replace or extend the instructions
setting_sources Load CLAUDE.md, skills and hooks from "project", "user" and so on
agents Subagent definitions (lesson 3)
hooks, mcp_servers, model, effort Callbacks, external tools, model choice, reasoning depth

The message stream

Message When
SystemMessage Subtype init at the start, compact_boundary after compaction
AssistantMessage Each content block Claude produces: text or a tool call
UserMessage Tool results sent back to Claude
StreamEvent Raw deltas, only with partial messages enabled
ResultMessage End of the loop: subtype, result, total_cost_usd, num_turns, session_id

Always check subtype before reading result, because result exists only on success.

subtype Meaning
success Finished normally
error_max_turns Hit max_turns
error_max_budget_usd Hit max_budget_usd
error_during_execution Interrupted, for example a cancelled request or a crash
error_max_structured_output_retries No valid structured output within the retry limit

The result also carries stop_reason from the last model response, so you can detect a refusal that still ended “successfully”.

Sessions and multi-turn use

Each query() starts a new session unless you pass resume=<session_id>, which you capture from the ResultMessage. In Python, ClaudeSDKClient keeps one session open across several client.query(...) calls, and it also lets you change the permission mode in the middle of a session. When context gets close to full, the SDK compacts older history automatically. Put rules that must survive compaction in CLAUDE.md (loaded through setting_sources), not only in the first prompt.

Recap

  • The Agent SDK is Claude Code’s loop and tools as a library: claude-agent-sdk (Python) and @anthropic-ai/claude-agent-sdk (TypeScript).
  • query() streams messages and ends with a ResultMessage. Check subtype before you read result.
  • allowed_tools approves, disallowed_tools removes, permission_mode decides everything else. Use dontAsk when nobody can answer a prompt.
  • Set max_turns and max_budget_usd yourself, because the defaults are unlimited.

$ ./try-it.sh

Try it: a CI-log triage agent with hard limits

  1. Run pip install claude-agent-sdk in a virtualenv (Python 3.10+) and export ANTHROPIC_API_KEY from your secret store.
  2. Put a failed build log in ./logs/build.log and save the script from this lesson as triage.py.
  3. Run python triage.py. Check that it prints success, the number of turns and the cost.
  4. Set max_turns=1 and run again. Confirm that you get error_max_turns, that result is missing, and that the except branch runs.
  5. Change the prompt to "Delete logs/build.log". Check that dontAsk denies the call because nothing allowed it, and that the run doesn't hang waiting for a prompt.

Check yourself

5 questions. Original practice questions, not taken from the real exam.

  1. A Python script runs an Agent SDK query in CI and needs to tell whether the agent finished or hit a limit.

    1. Which field should it check?

  2. A teammate configures allowed_tools=["Read", "Grep"] and permission_mode="bypassPermissions" and says that the agent can now only read files.

    2. What actually happens?

  3. A nightly Python job loops over query(). When the agent hits max_turns, the log shows "error_max_turns" and then the job dies with an unhandled exception.

    3. What's the cause, and what's the fix?

  4. You're putting an Agent SDK agent into a shared CI runner, and finance wants a guarantee that one bad prompt can't run for hours.

    4. Which two options directly bound a run? (Choose two.)

    Choose 2.

  5. A long-running SDK session was told in its first prompt "never touch the prod workspace". Two hours later it proposes a change to prod, and the stream shows a compact_boundary system message.

    5. What is the most likely cause, and what is the best fix?

Answer every question to check.