Module 2.2
Claude Agent SDK
The Shift in Abstraction
In Module 2.1, you wrote the agent loop yourself. You checked stop_reason, extracted tool calls, ran functions, fed results back, and kept going until Claude reached end_turn. That loop is fundamental to understand — but writing it from scratch every time is tedious.
The Claude Agent SDK is what you get when Anthropic packages that loop for you. Instead of managing the back-and-forth, you give Claude a task and it works autonomously until it's done. Built-in tools — reading files, running bash, searching the web — are already wired in. You just tell Claude which ones it's allowed to use.
The difference in practice:
# Client SDK: you implement the loop
response = client.messages.create(tools=tools, messages=messages)
while response.stop_reason == "tool_use":
result = execute_tool(response)
response = client.messages.create(tool_result=result, ...)
# Agent SDK: Claude handles it
async for message in query(prompt="Find and fix the bug in auth.py"):
print(message)When to use which: The Client SDK gives you fine-grained control — every decision goes through your code. The Agent SDK gives you autonomous execution — Claude decides what to do next. Use the Client SDK for predictable, controlled pipelines. Use the Agent SDK when you want Claude to genuinely work through a task on its own.
The query() Function
Everything goes through query(). It's async and yields messages as the agent works.
pip install claude-agent-sdkimport asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def main():
async for message in query(
prompt="Read all Python files in this directory and summarize what each one does",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Glob"]
),
):
if hasattr(message, "result"):
print(message.result)
asyncio.run(main())allowed_tools is your permission boundary. List only what the task actually needs. An agent reading files doesn't need Bash or Write — giving it those creates unnecessary risk.
| Tool | What it does |
|---|---|
Read | Read any file |
Write | Create new files |
Edit | Precisely edit existing files |
Bash | Run terminal commands |
Glob | Find files by pattern |
Grep | Search file contents |
WebSearch | Search the web |
WebFetch | Fetch and parse a URL |
AskUserQuestion | Ask you a clarifying question |
Task | Invoke a subagent |
Task is the most important one on that list — and the one people most commonly forget to include.
Subagents
A subagent is a separate Claude instance — its own context window, its own system prompt, its own tool restrictions. The main agent delegates to a subagent for focused subtasks; the subagent works independently and returns a summary.
The result: verbose, noisy work happens in the subagent's context, not yours. Your main agent stays clean and focused.
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
async for message in query(
prompt="Review auth.py for security issues, then run the test suite",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Bash", "Task"], # Task REQUIRED for subagents
agents={
"code-reviewer": AgentDefinition(
description="Reviews code for security vulnerabilities and quality issues. Use when reviewing any source file.",
prompt="""You are a security-focused code reviewer.
Check for: hardcoded secrets, injection vulnerabilities, missing input validation, auth bypasses.
Output: Critical Issues → Warnings → Verdict (APPROVE or REQUEST CHANGES)""",
tools=["Read", "Grep", "Glob"], # read-only
model="sonnet",
),
"test-runner": AgentDefinition(
description="Runs the test suite and reports results. Use after any code change.",
prompt="Run the tests. Report which pass, which fail, and the root cause of failures.",
tools=["Bash", "Read"],
model="haiku",
),
},
),
):
if hasattr(message, "result"):
print(message.result)The rule that breaks everyone: Task must be in allowed_tools. Without it, subagents are defined but never invoked — they sit there silently, and you'll wonder why Claude isn't delegating.
The second rule: subagents cannot spawn other subagents. Never put Task in a subagent's tools.
Hooks
The same hook system as Claude Code, but as Python callbacks instead of shell scripts.
from claude_agent_sdk import HookMatcher
async def block_env_writes(input_data, tool_use_id, context):
file_path = input_data["tool_input"].get("file_path", "")
if file_path.endswith(".env"):
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Modifying .env files is not allowed",
}
}
return {} # empty dict = proceed normally
async for message in query(
prompt="Update the database configuration",
options=ClaudeAgentOptions(
hooks={
"PreToolUse": [
HookMatcher(matcher="Write|Edit", hooks=[block_env_writes])
]
}
),
):
print(message)Return {} and Claude proceeds. Return permissionDecision: "deny" and the tool call is blocked. Return updatedInput and you modify the tool's arguments before execution.
Why this matters: You can't rely on a prompt saying "don't touch .env files." Prompts get ignored under pressure. Hooks are code — they always run.
Sessions
By default, every query() call starts fresh. Sessions let you resume — Claude picks up with full context from the previous call.
session_id = None
# First query — research the codebase
async for message in query(
prompt="Read and analyze the authentication module. Understand how sessions work.",
options=ClaudeAgentOptions(allowed_tools=["Read", "Glob", "Grep"]),
):
if hasattr(message, "subtype") and message.subtype == "init":
session_id = message.session_id # capture this
# Second query — Claude remembers everything from the first
async for message in query(
prompt="Now find every caller of the session functions you just analyzed",
options=ClaudeAgentOptions(resume=session_id),
):
if hasattr(message, "result"):
print(message.result)Think of it like a conversation with a contractor. The first call is the briefing. Every subsequent call with the same session_id is a follow-up — they already know the context.
Where Things Go Wrong
Task not in allowed_tools. Subagents are silently never invoked. No error, no warning.
Hook event names are case-sensitive. PreToolUse works. pretooluse does nothing.
Subagents don't inherit parent permissions. Each subagent gets its own permission prompts unless configured. Unhandled prompts will stall an overnight autonomous run.
SessionStart, SessionEnd, Notification hooks are TypeScript-only. Python SDK does not support them.
Windows path length limit. Subagent system prompts over ~8191 characters can fail silently. Keep AgentDefinition.prompt concise.
Sources
- Claude Agent SDK Overview
- Agent SDK Hooks
- Agent SDK Subagents
- Building Agents with the Claude Agent SDK