Four ways to run an agent
- You, in full detailClient SDKMessages API, your own loop
- A library you embedAgent SDKPython or TypeScript, your process
- A person at a terminalClaude Code CLIinteractive or headless
-p - Anthropic, hostedManaged Agentshosted harness and sandbox
What the Agent SDK actually is
The Claude Agent SDK is Claude Code as a library. It gives you the same tools, agent loop and context management that power Claude Code, programmable in Python and TypeScript, running in a process you operate. Both SDKs bundle a native Claude Code binary, so the library you install is also the harness that runs.
The capabilities it brings are the ones you would otherwise have to write: built-in tools (Read, Write, Edit, Bash, Glob, Grep, WebSearch, WebFetch and the orchestration tools), a permission system, hooks, subagents, MCP connections, sessions you can resume or fork, and automatic compaction when the context window fills. Skills, slash commands and memory files load from .claude/ directories exactly as they do in Claude Code.
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def main():
async for message in query(
prompt="Find and fix the failing tests in the auth module",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Edit", "Bash", "Grep"], # auto-approved
setting_sources=["project"], # load CLAUDE.md, skills, hooks
max_turns=30, # backstop against a runaway loop
max_budget_usd=5.0, # spend cap, subagents included
),
):
if isinstance(message, ResultMessage):
# Only "success" carries the final text in .result
print(message.subtype, message.session_id, message.total_cost_usd)
asyncio.run(main())Read what that loop hands back. The SDK streams messages — a SystemMessage with subtype init at session start, an AssistantMessage per content block, a UserMessage carrying each tool result — and ends with a ResultMessage. Its subtype is the termination state: success, error_max_turns, error_max_budget_usd, error_during_execution. Only success carries the result text, so checking the subtype before reading the answer is not defensive style, it is correctness.
Writing your own harness
A harness is the code around the model: it sends the request, executes tools, appends results and decides when to stop. Writing your own against the Messages API is entirely reasonable — it is how you get a loop in a language the Agent SDK does not cover, or one shaped around your own state machine. The Client SDKs also offer a beta tool runner that drives the loop for you if you would rather not.
messages = [{"role": "user", "content": task}]
while True:
resp = client.messages.create(
model=MODEL, max_tokens=4096, tools=TOOLS, messages=messages,
)
messages.append({"role": "assistant", "content": resp.content})
if resp.stop_reason != "tool_use":
break # end_turn, max_tokens, refusal — handle each
results = [
{"type": "tool_result", "tool_use_id": b.id, "content": run_tool(b)}
for b in resp.content if b.type == "tool_use"
]
messages.append({"role": "user", "content": results})That is eighteen lines and it works. What it does not yet have is everything the Agent SDK ships with: permission checks before a tool runs, compaction when the window fills, session persistence and resumption, subagents, parallel execution of read-only tools, cost accounting. Building a harness means choosing to own that list.
What you own in each option
Your own harness
- You read
stop_reasonand match everytool_use_id - You write permissioning, retries and trimming
- You persist sessions and handle resumption
- Full control of every byte sent to the API
Agent SDK
- The loop ends when Claude replies with no tool calls
- Permissions, hooks and compaction are built in
session_idresumes or forks a conversation- You still run the process and hold the API key
Self-hosted versus Anthropic-hosted
“Self-hosted” here means the agent loop and its tools run on infrastructure you operate — your container, your laptop, your VPC — whichever library you used. The CLI, the Agent SDK and a hand-written harness are all self-hosted in that sense. Anthropic-hosted means Managed Agents: a pre-built, configurable harness that runs in Anthropic's infrastructure, where you define an agent (model, system prompt, tools, MCP servers, skills), define an environment, start a stateful session, and stream events over server-sent events.
| Client SDK loop | Agent SDK | Managed Agents | |
|---|---|---|---|
| Who runs the loop | You | The SDK, in your process | Anthropic |
| Where tools execute | Your code | Your machine or container | A cloud sandbox, or a self-hosted sandbox you run |
| State between turns | Whatever you store | Sessions you can resume or fork | Stateful sessions with a persistent filesystem |
| Best for | Custom loops, fine-grained control | Embedding a coding-capable agent in your app | Long-running and asynchronous work with little infrastructure |
Hooks: the deterministic part of an agent
A hook is a callback that fires at a fixed point in the loop and whose return value the harness obeys. PreToolUse fires before a tool executes and can allow, deny, ask or rewrite the call. PostToolUse fires after and can rewrite the output or append context. UserPromptSubmit can inject context into a prompt. Stop, SubagentStart, SubagentStop, PreCompact and others cover the rest of the lifecycle. Hooks run in your application's process, not in the model's context window, so they cost no tokens.
async def protect_env_files(input_data, tool_use_id, context):
path = input_data["tool_input"].get("file_path", "")
if path.split("/")[-1] == ".env":
return {"hookSpecificOutput": {
"hookEventName": input_data["hook_event_name"],
"permissionDecision": "deny",
"permissionDecisionReason": "Cannot modify .env files",
}}
return {} # empty object == allow unchanged
options = ClaudeAgentOptions(hooks={
"PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])]
})Note the matcher. "Write|Edit" means the callback only fires for those tools; a hook with no matcher fires for every event of its type. When the hook denies, Claude receives the rejection as the tool result and typically tries another approach or explains that it cannot proceed — the model is told, not silently ignored.
A hook intercepting a tool call
PreToolUse hookWrite to .envdeny + reasonPrecedence when decisions disagree
strongest decision at the top
denyblocks, whatever else saiddeferends the query to decide lateraskroutes to a human decisionallowruns the call
deny anywhere blocks the operation.Traps the wrong answers are built from
| Tempting but wrong | Do this instead |
|---|---|
| Enforcing a safety rule in the system prompt | Enforce it in a PreToolUse hook that returns deny; keep the prompt for steering. |
Reading ResultMessage.result without checking subtype | Branch on the subtype first — only success carries the final text. |
| Writing a bespoke harness to get features the SDK already has | Write your own loop for control you actually need, not for permissions, compaction and sessions. |
| Choosing hosted or self-hosted on developer convenience | Decide on where the data and the execution are allowed to live, then pick the option that fits. |
| Doing slow work inside a blocking hook | Return an async output for pure side effects such as logging, so the loop continues. |
You should now be able to
- Describe what the Claude Agent SDK provides over a hand-written Messages API loop.
- Write a minimal agent with
query()and read theResultMessagecorrectly. - Choose between a custom harness, the Agent SDK and Anthropic-hosted Managed Agents for a given constraint.
- Register a
PreToolUsehook that denies a tool call, and explain why that beats a prompt instruction. - Name the main hook events and the order in which conflicting decisions are resolved.