# Agent SDK Cheat Sheet

Claude Agent SDK quickstarts in Python and TypeScript, options, hooks, subagents, MCP servers, permission modes, streaming events and headless patterns.

import { Tabs, TabItem, Steps } from '@prosefly/astro-components';

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.

:::note[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

```bash
pip install claude-agent-sdk          # Python
npm install @anthropic-ai/claude-agent-sdk   # TypeScript
```

## Quickstart

<Tabs>
  <TabItem label="Python">
```python
import anyio
from 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)
```
  </TabItem>
  <TabItem label="TypeScript">
```typescript
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);
}
```
  </TabItem>
</Tabs>

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

:::caution[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:

<Tabs>
  <TabItem label="Python">
```python
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"])
```
  </TabItem>
  <TabItem label="TypeScript">
```typescript
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' };
  },
};
```
  </TabItem>
</Tabs>

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

```python
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:

```python
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

```python
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.

```python
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` |

```python
# CI gate sketch
import sys
async 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.

<Steps>
1. **Self-hosted + VPC data locality** → Agent SDK, not Managed Agents (which is hosted). A Messages API loop would mean rebuilding the harness.
2. **Must never edit** → `permission_mode="plan"` (read-only) and a tool allowlist without `Edit`/`Write`.
3. **Security focus** → a `security-reviewer` subagent on `claude-sonnet-5` (cheaper, sufficient), tools `Read`, `Grep`, `Bash(git diff:*)`.
4. **Enforcement** → a `PreToolUse` deny for any write, deterministic, not a prompt sentence.
5. **Fail the build** → parse the `result` message; `sys.exit(1)` on a finding.
6. **Cost/loop safety** → cap `max_turns`; log `total_cost_usd`; still branch on `stop_reason`.
</Steps>

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_turns` is 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.
- `bypassPermissions` is for sandboxed CI only.
