Appendix · Claude
Agent SDK Cheat Sheet
Claude Agent SDK quickstarts in Python and TypeScript, options, hooks, subagents, MCP servers, permission modes, streaming events and headless patterns.
The Claude Agent SDK (claude-agent-sdk, renamed from “Claude Code SDK”) exposes the Claude Code agent harness programmatically: the agentic loop, tools, hooks, permissions, subagents and MCP servers. You host it — contrast Managed Agents, where Anthropic hosts the loop and sandbox.
When to reach for the SDK
Choose the Agent SDK when you need to control the runtime, network, data locality or the harness itself. Choose Managed Agents for least operational overhead. Choose a plain Messages API loop when you do not need Claude Code’s file/tool machinery.
Install
pip install claude-agent-sdk # Pythonnpm install @anthropic-ai/claude-agent-sdk # TypeScriptQuickstart
import anyiofrom claude_agent_sdk import query, ClaudeAgentOptions
async def main(): options = ClaudeAgentOptions( model="claude-opus-5", system_prompt="You are a precise refactoring assistant.", allowed_tools=["Read", "Edit", "Bash"], permission_mode="acceptEdits", cwd="/repo", ) async for message in query(prompt="Rename getUserName to getUsername repo-wide, then run tests.", options=options): print(message)
anyio.run(main)import { query } from '@anthropic-ai/claude-agent-sdk';
for await (const message of query({ prompt: 'Rename getUserName to getUsername repo-wide, then run tests.', options: { model: 'claude-opus-5', systemPrompt: 'You are a precise refactoring assistant.', allowedTools: ['Read', 'Edit', 'Bash'], permissionMode: 'acceptEdits', cwd: '/repo', },})) { console.log(message);}Options
| Option (Py / TS) | Purpose |
|---|---|
model | Model ID (claude-opus-5, claude-sonnet-5, …) |
system_prompt / systemPrompt | Base instructions; may append to the built-in Claude Code prompt |
allowed_tools / allowedTools | Tool allowlist (Read, Edit, Bash, Grep, …) |
disallowed_tools / disallowedTools | Explicit denies |
permission_mode / permissionMode | default | acceptEdits | plan | bypassPermissions |
cwd | Working directory |
mcp_servers / mcpServers | MCP server configs available to the agent |
hooks | Lifecycle handlers (see below) |
agents | Subagent definitions |
setting_sources / settingSources | Whether to load CLAUDE.md/settings from disk |
max_turns / maxTurns | Safety cap on agentic turns (not the primary stop) |
env | Environment variables for tool execution |
max_turns is a guard, not a stop
max_turns is a runaway safety net. The real loop termination is still stop_reason: end_turn. Using a turn cap as the primary stop is anti-pattern 2.
Permission modes
| Mode | Behaviour | Use |
|---|---|---|
default | Ask per the permission rules | Interactive, cautious |
acceptEdits | Auto-accept file edits, ask for shell | Trusted edit loops |
plan | Read-only; produce a plan, execute nothing | Review before acting |
bypassPermissions | No prompts | Sandboxed CI only |
Programmatic permission callback
Fine-grained control beyond allow/deny lists — decide per call:
async def can_use_tool(tool_name, tool_input, context): if tool_name == "Bash" and "rm -rf" in tool_input.get("command", ""): return {"behavior": "deny", "message": "destructive command blocked"} if tool_name == "Edit" and tool_input.get("file_path", "").endswith(".env"): return {"behavior": "deny", "message": "secrets are read-only"} return {"behavior": "allow"}
options = ClaudeAgentOptions(can_use_tool=can_use_tool, allowed_tools=["Read", "Edit", "Bash"])const options = { allowedTools: ['Read', 'Edit', 'Bash'], canUseTool: async (toolName: string, input: Record<string, unknown>) => { if (toolName === 'Bash' && String(input.command).includes('rm -rf')) return { behavior: 'deny', message: 'destructive command blocked' }; return { behavior: 'allow' }; },};Hooks
Same lifecycle events as Claude Code (PreToolUse, PostToolUse, UserPromptSubmit, Stop, SessionStart, SubagentStop, PreCompact, Notification), registered in code. A PreToolUse hook returning a deny decision blocks the tool — the deterministic enforcement mechanism.
async def block_destructive(input_data, tool_use_id, context): cmd = input_data.get("tool_input", {}).get("command", "") if any(bad in cmd for bad in ("rm -rf", "git push --force", "drop table")): return {"decision": "block", "reason": "destructive command"} return {}
options = ClaudeAgentOptions( hooks={"PreToolUse": [{"matcher": "Bash", "hooks": [block_destructive]}]})Subagents
Define delegated agents with isolated context, their own tools and model:
options = ClaudeAgentOptions( agents={ "security-reviewer": { "description": "Reviews diffs for injection, secrets, authz. Use after auth edits.", "prompt": "Report findings only: file:line, severity, fix. Do not edit.", "tools": ["Read", "Grep", "Glob"], "model": "claude-sonnet-5", } })Subagents receive only what the parent passes; never assume inheritance. Use a cheaper model for mechanical delegated work.
MCP servers
options = ClaudeAgentOptions( mcp_servers={ "orders": {"command": "python", "args": ["orders_server.py"]}, # stdio "linear": {"type": "http", "url": "https://mcp.linear.app/mcp"}, # remote }, allowed_tools=["mcp__orders__get_order", "mcp__linear__list_issues"])MCP tools are namespaced mcp__<server>__<tool>; allowlist them explicitly.
Streaming events
query() yields a stream of typed messages mirroring the SSE flow: a system init message, assistant messages (text/tool_use blocks), user messages (tool_result), and a final result message carrying stop_reason, usage and cost.
async for message in query(prompt="…", options=options): if message.type == "assistant": for block in message.content: if block.type == "text": print(block.text, end="") elif message.type == "result": print(message.stop_reason, message.usage, message.total_cost_usd)Branch on the result message’s stop_reason, not on parsing assistant text.
Headless patterns
| Pattern | How |
|---|---|
| One-shot batch job | query() once, read the result message, exit non-zero on failure |
| CI gate | permission_mode="plan" or a tight allowlist; parse result for pass/fail |
| Fan-out over units | Spawn N SDK sessions (one per file/module), each in its own worktree, aggregate |
| Long-running service | Persist the memory tool store; use context editing to bound tokens |
| Cost control | Cheaper model for subagents; cap max_turns; log total_cost_usd |
# CI gate sketchimport sysasync def review(): async for m in query(prompt="Review the diff for security issues; output PASS or FAIL.", options=ClaudeAgentOptions(permission_mode="plan", allowed_tools=["Read", "Grep", "Bash(git diff:*)"])): if m.type == "result": sys.exit(0 if "PASS" in (m.result or "") else 1)Agent SDK vs Managed Agents vs Messages API loop
| Messages API loop | Agent SDK | Managed Agents | |
|---|---|---|---|
| You write the loop | ✓ | Harness provided | No (hosted) |
| File tools / Claude Code machinery | No | ✓ | ✓ |
| Hooks / subagents / permission modes | Build yourself | ✓ | Configured |
| Runtime/network/data control | Full | Full | Limited |
| Ops burden | Low (just API) | You host harness | Minimal |
| Choose when | Simple tool loop | Need the harness, self-hosted | Least ops |
Common misconceptions
| Misconception | Reality | Why it matters on the exam |
|---|---|---|
| “Agent SDK is Anthropic-hosted” | You host it; Managed Agents is hosted | Hosting-confusion distractor |
“max_turns is how the loop stops” | It is a safety cap; stop_reason stops the loop | Iteration-cap anti-pattern |
“bypassPermissions is fine for convenience” | Sandboxed CI only | Excessive-agency distractor |
| “Subagents see the parent’s whole context” | Only what is passed explicitly | Silent-context-loss distractor |
| “Enforce rules in the system prompt” | Use hooks / permission callbacks | Prompt-as-enforcement anti-pattern |
| “Load every MCP tool” | Allowlist namespaced tools; keep it small | Too-many-tools anti-pattern |
Scenario walkthrough
A platform team wants a self-hosted CI agent that reviews PRs for security issues, must never edit files, must run inside their VPC (data locality), and must fail the build on a finding.
- Self-hosted + VPC data locality → Agent SDK, not Managed Agents (which is hosted). A Messages API loop would mean rebuilding the harness.
- Must never edit →
permission_mode="plan"(read-only) and a tool allowlist withoutEdit/Write. - Security focus → a
security-reviewersubagent onclaude-sonnet-5(cheaper, sufficient), toolsRead,Grep,Bash(git diff:*). - Enforcement → a
PreToolUsedeny for any write, deterministic, not a prompt sentence. - Fail the build → parse the
resultmessage;sys.exit(1)on a finding. - Cost/loop safety → cap
max_turns; logtotal_cost_usd; still branch onstop_reason.
Rejected alternatives: Managed Agents (violates data locality), bypassPermissions (excessive agency), enforcing read-only via the system prompt (prompt-as-enforcement), and using a turn cap as the completion signal (iteration-cap anti-pattern).
Key takeaways
- The Agent SDK is self-hosted; Managed Agents is Anthropic-hosted; pick on runtime/data-control vs ops-burden.
- Loop termination is
stop_reason;max_turnsis only a runaway guard. - Enforce rules with hooks /
can_use_tool, never with prompt text. - Subagents get only explicitly-passed context; give them their own (often cheaper) model and a tight tool allowlist.
- MCP tools are
mcp__server__tool; allowlist them and keep the set small. bypassPermissionsis for sandboxed CI only.
Last updated Sep 18, 2026