AI Engineering Curriculum
Phase 2: Single AI Agent Development·5 min read

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:

Python
# 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.

Bash
pip install claude-agent-sdk
Python
import 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.

ToolWhat it does
ReadRead any file
WriteCreate new files
EditPrecisely edit existing files
BashRun terminal commands
GlobFind files by pattern
GrepSearch file contents
WebSearchSearch the web
WebFetchFetch and parse a URL
AskUserQuestionAsk you a clarifying question
TaskInvoke 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.

Python
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.

Python
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.

Python
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