Anthropic's claude-agent-sdk: A Practical Walkthrough
The claude-agent-sdk is Anthropic's productized version of the harness that powers Claude Code. It gives you the same agent loop, tool dispatch, and context-management mechanics, programmable in Python and TypeScript. If you've been wiring up tool-use loops by hand against the Messages API, this is the layer you've been re-implementing badly.
This walkthrough covers what's in it, when to use it, and the gotchas worth knowing before you commit.
What you actually get
The SDK bundles four things:
defer permission decision the runtime understands. [Inference]Setup
Python
pip install claude-agent-sdkThe Python SDK latest release as of May 2026 includes hook event streaming and parity with the TypeScript SDK on permission decisions.
TypeScript
npm install @anthropic-ai/claude-agent-sdkThe TypeScript SDK ships a native Claude Code binary as an optional dependency, so you don't need to install Claude Code separately.
API key
Set ANTHROPIC_API_KEY in your environment. Opus 4.7 (claude-opus-4-7) requires Agent SDK v0.2.111 or later — older SDK versions reject the model.
A minimal agent
Python:
from claude_agent_sdk import Agent, tool
@tool
def get_weather(city: str) -> str:
"""Return current weather for a city."""
return f"Sunny, 72F in {city}"
agent = Agent(
model="claude-opus-4-7",
system="You are a helpful weather assistant.",
tools=[get_weather],
)
result = agent.run("What's the weather in Paris?")
print(result.text)The SDK handles the multi-turn loop — tool call, result, follow-up — without you writing a step controller.
TypeScript:
import { Agent, tool } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
const getWeather = tool({
name: "get_weather",
description: "Return current weather for a city.",
schema: z.object({ city: z.string() }),
handler: async ({ city }) => `Sunny, 72F in ${city}`,
});
const agent = new Agent({
model: "claude-opus-4-7",
system: "You are a helpful weather assistant.",
tools: [getWeather],
});
const result = await agent.run("What's the weather in Paris?");
console.log(result.text);Streaming
Streaming is first-class:
async for event in agent.run_stream("Plan a trip to Lisbon"):
if event.type == "text":
print(event.text, end="", flush=True)
elif event.type == "tool_use":
print(f"[calling {event.name}]")Events come through as a typed union — text deltas, tool-use start, tool-result, and end-of-turn markers. Use this for any user-facing surface where time-to-first-token matters.
Hooks
The hook system is where the SDK earns its keep. A pre-tool-use hook can:
return {permissionDecision: "deny"})"allow")"defer")def gate_writes(event):
if event.tool_name in ("write_file", "delete_file"):
return {"permissionDecision": "defer"}
return {"permissionDecision": "allow"}
agent = Agent(
...,
hooks={"pre_tool_use": gate_writes},
)This is how Claude Code itself implements its permission model. You can build the same pattern for any sensitive tool — DB writes, money movement, anything that should be human-confirmed.
Post-tool-use hooks fire after execution, ideal for logging into your observability backend or running tool-result validation.
When to use claude-agent-sdk vs raw Messages API
Use claude-agent-sdk when:
Use the raw Messages API when:
The split is similar to "stdlib HTTP vs requests / fetch" — both work, the higher level removes a lot of boilerplate but adds opinions.
Gotchas
A few sharp edges worth knowing:
include_hook_events; without it you don't see hook events in the stream. Easy to miss.