Large Language ModelsGenerate imagesGenerate videos
Claude Agent SDK Tutorial: Python and TypeScript Examples
Install the Claude Agent SDK, run your first query in Python and TypeScript, then add custom tools, subagents, hooks, spend limits and resumable sessions. Every example is checked against the current documentation and ready to paste into a project.
The Claude Agent SDK lets you run the same agent loop that powers Claude Code from inside your own Python or TypeScript program. You send one prompt, and Claude reads files, runs commands, edits code and calls your own functions until the job is finished, streaming every step back to you as it works. This tutorial builds the SDK up in layers: a first query in each language, custom tools, subagents, hooks, spend limits and resumable sessions. Every snippet follows the current documentation, so you can paste it, set one environment variable and run it.
What the Agent SDK Does
An agent is a program that decides its own next step. It reads a request, picks a tool, looks at the result and keeps going until the job is done. The Agent SDK hands you that loop ready-made: the same built-in tools, permission system, context handling and hooks that run inside Claude Code, exposed as a library for Python and TypeScript.
The practical difference from calling the raw API is who writes the loop. With the Client SDK you send a message, check whether Claude asked for a tool, run it, send the result back and repeat. With the Agent SDK you call query() once and iterate over the messages it streams while Claude does the work.
Each turn of that loop follows the same rhythm. Claude receives the prompt and the tool definitions, then decides whether to answer or call a tool. The SDK runs the tool and feeds the result back, and the cycle repeats until Claude has nothing left to do or a limit stops it. Every step arrives in your code as a message, which is why the examples below look like stream processing rather than a single request and response. It also means you can show progress to a user, log each tool call or stop early.
SDK, Client SDK, or CLI
Four options sound similar, so this table sorts them by who runs the agent.
You want to
Use
What you get
Embed an agent in your own Python or TypeScript app
Agent SDK
The Claude Code agent loop as a library, with built-in tools, permissions, sessions and hooks
Work interactively from a terminal
Claude Code CLI
A terminal interface built for daily use and one-off tasks
Call the Claude API from your own code
Client SDK
Direct API access where you write the tool loop yourself
Let Anthropic host the agent
Managed Agents
A hosted harness that runs the loop in a managed sandbox
💡 Tip: To drive the same loop from another language, run the CLI as a subprocess with the -p flag and --output-format json.
What You Need First
Three things must be in place before the first line of code:
Python 3.10+ or Node.js 18+
An Anthropic account with an API credential from the Claude Console
A folder of code for the agent to work on, because by default it can read files in its working directory and subdirectories
Both packages bundle a native Claude Code binary, so a normal install needs nothing extra. Two situations break that: pip falling back to the source distribution (ARM64 Windows is the usual case) and an npm install that skips optional dependencies. In both, install Claude Code natively and the SDK will find it.
Install and Authenticate
Install the Package
For Python, create a virtual environment and install the package:
Setting "type": "module" allows top-level await in your script, and tsx runs TypeScript files without a build step.
Set Your Credentials
The SDK reads your credential from an environment variable in the shell that runs the agent:
export ANTHROPIC_API_KEY=your-api-key
$env:ANTHROPIC_API_KEY = "your-api-key"
The SDK does not load .env files on its own. If you keep the credential in one, load it first with python-dotenv or the dotenv package. Cloud providers work too: set CLAUDE_CODE_USE_BEDROCK=1 for Amazon Bedrock, CLAUDE_CODE_USE_VERTEX=1 for Google Cloud, or CLAUDE_CODE_USE_FOUNDRY=1 for Microsoft Foundry, then configure that provider's credentials.
💡 Policy note: Unless Anthropic has approved it beforehand, third-party products built on the SDK may not offer claude.ai login or its rate limits. Use API credentials for anything you ship.
Your First Agent in Two Languages
Create a file called utils.py with two deliberate bugs. An empty list crashes the average, and a missing user crashes the name lookup:
def calculate_average(numbers):
total = 0
for num in numbers:
total += num
return total / len(numbers)
def get_user_name(user):
return user["name"].upper()
The agent below is read-only. It can look at the file, but it has no permission to change anything.
The Python Version
import asyncio
from claude_agent_sdk import (
query,
ClaudeAgentOptions,
AssistantMessage,
ResultMessage,
TextBlock,
)
async def main():
options = ClaudeAgentOptions(
system_prompt="You review Python code and report crash risks in plain language.",
allowed_tools=["Read", "Glob", "Grep"],
max_turns=8,
)
async for message in query(
prompt="Look at utils.py and list every input that would crash it.",
options=options,
):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(block.text)
elif isinstance(message, ResultMessage):
print(f"Finished: {message.subtype}, cost: ${message.total_cost_usd}")
asyncio.run(main())
Here is what each part does:
query() returns an async iterator, so you loop with async for while Claude thinks, calls tools and reads results
allowed_tools pre-approves Read, Glob and Grep, three tools that can inspect files but never modify them
AssistantMessage blocks carry Claude's text and its tool calls, so filtering for TextBlock gives you readable output
ResultMessage arrives last, with subtype, total_cost_usd, num_turns and session_id
The TypeScript Version
Save this as agent.ts and run it with npx tsx agent.ts:
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Look at utils.py and list every input that would crash it.",
options: {
systemPrompt: "You review Python code and report crash risks in plain language.",
allowedTools: ["Read", "Glob", "Grep"],
maxTurns: 8
}
})) {
if (message.type === "assistant" && message.message?.content) {
for (const block of message.message.content) {
if ("text" in block) console.log(block.text);
}
} else if (message.type === "result") {
console.log(`Finished: ${message.subtype}, cost: $${message.total_cost_usd}`);
}
}
Run either script and you should see Claude read utils.py, describe both crashes (the division by zero on an empty list and the TypeError on a missing user) and end with a line such as Finished: success. If you see an authentication error such as Not logged in, the environment variable is missing from the shell that launched the script, which is the most common first-run error.
The logic is identical in both languages, but the option names change case. Keep this table nearby, because it explains most "why does this not work" moments when you port code between the two SDKs.
Setting
Python
TypeScript
Pre-approved tools
allowed_tools
allowedTools
Permission mode
permission_mode
permissionMode
Turn limit
max_turns
maxTurns
Spend limit
max_budget_usd
maxBudgetUsd
Custom tool servers
mcp_servers
mcpServers
Resume a session
resume
resume
Subagents
agents
agents
Tool lists are how you dial autonomy up or down:
Tools
What the agent can do
Read, Glob, Grep
Look at code and change nothing
Read, Edit, Glob
Look at code and modify it
Read, Edit, Bash, Glob, Grep
Run end to end, including shell commands
💡 Two behaviors to expect: A single-shot query() raises an exception after it yields an error result, such as hitting the turn limit, so wrap the loop in try/except or try/catch when the script must keep running. And by default the SDK reads your project's .claude/ folder and ~/.claude/, the way the CLI does, so settings, skills and hooks defined there apply to your agent too.
Build Custom Tools
Built-in tools handle files and the shell. Your own logic, such as a database lookup or an internal API call, belongs in a custom tool: a function wrapped in an in-process MCP server that runs inside your application, not as a separate process.
A tool has four parts: a name, a description that Claude reads to decide when to call it, an input schema, and an async handler that returns a content array. Write the description the way you would brief a new colleague.
You register the server through mcp_servers (mcpServers in TypeScript). The name you give the server in that dictionary becomes part of the tool's full name, using the pattern mcp__{server}__{tool}. Put that full name in your allowed tools and the call runs without a permission prompt.
A Python Tool
import asyncio
from typing import Any
from claude_agent_sdk import (
tool,
create_sdk_mcp_server,
query,
ClaudeAgentOptions,
ResultMessage,
)
ORDERS = {"A100": "shipped", "A101": "packing"}
@tool("get_order_status", "Look up the status of an order by its id", {"order_id": str})
async def get_order_status(args: dict[str, Any]) -> dict[str, Any]:
status = ORDERS.get(args["order_id"])
if status is None:
return {
"content": [{"type": "text", "text": f"No order {args['order_id']}"}],
"is_error": True,
}
return {"content": [{"type": "text", "text": f"Order {args['order_id']}: {status}"}]}
shop_server = create_sdk_mcp_server(
name="shop", version="1.0.0", tools=[get_order_status]
)
async def main():
options = ClaudeAgentOptions(
mcp_servers={"shop": shop_server},
allowed_tools=["mcp__shop__get_order_status"],
)
async for message in query(prompt="Where is order A100?", options=options):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)
asyncio.run(main())
A TypeScript Tool
TypeScript describes tool inputs with Zod, so run npm install zod first:
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
const orders: Record<string, string> = { A100: "shipped", A101: "packing" };
const getOrderStatus = tool(
"get_order_status",
"Look up the status of an order by its id",
{ order_id: z.string().describe("Order id such as A100") },
async (args) => {
const status = orders[args.order_id];
if (!status) {
return {
content: [{ type: "text", text: `No order ${args.order_id}` }],
isError: true
};
}
return { content: [{ type: "text", text: `Order ${args.order_id}: ${status}` }] };
}
);
const shopServer = createSdkMcpServer({
name: "shop",
version: "1.0.0",
tools: [getOrderStatus]
});
for await (const message of query({
prompt: "Where is order A100?",
options: {
mcpServers: { shop: shopServer },
allowedTools: ["mcp__shop__get_order_status"]
}
})) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}
💡 Tip: For expected failures like an unknown order id, return is_error: True (isError: true in TypeScript) with your own message. Claude reads that message and can retry or explain, instead of receiving a bare exception string.
Subagents, Hooks, and Limits
One working agent is a start. Three features keep a bigger one predictable: subagents split the work, hooks police every tool call, and limits stop a runaway session.
Delegate to Subagents
A subagent is a separate agent instance that your main agent spawns for a focused job. Each one starts with a fresh context, so a reviewer can read dozens of files without bloating the main conversation, and only its final message comes back to the parent. You define them with the agents option and add Agent to the allowed tools.
Each definition needs a description (when Claude should use it) and a prompt (its behavior). Optional fields include tools to restrict what it can touch and model, which accepts the aliases sonnet, opus, haiku, fable and inherit, or a full model ID.
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
async def main():
options = ClaudeAgentOptions(
allowed_tools=["Read", "Grep", "Glob", "Agent"],
agents={
"code-reviewer": AgentDefinition(
description="Reviews code for security and maintainability problems.",
prompt="You are a careful reviewer. Report concrete problems with file names.",
tools=["Read", "Grep", "Glob"],
model="sonnet",
),
"test-runner": AgentDefinition(
description="Runs the test suite and summarizes failures.",
prompt="Run the tests, then list each failing test with its error.",
tools=["Bash", "Read", "Grep"],
),
},
)
async for message in query(
prompt="Use the code-reviewer agent to check the auth module",
options=options,
):
if hasattr(message, "result"):
print(message.result)
asyncio.run(main())
The TypeScript version uses plain objects:
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Use the code-reviewer agent to check the auth module",
options: {
allowedTools: ["Read", "Grep", "Glob", "Agent"],
agents: {
"code-reviewer": {
description: "Reviews code for security and maintainability problems.",
prompt: "You are a careful reviewer. Report concrete problems with file names.",
tools: ["Read", "Grep", "Glob"],
model: "sonnet"
}
}
}
})) {
if ("result" in message) console.log(message.result);
}
Naming the subagent in your prompt, as above, guarantees Claude uses it. Without that, Claude matches the task against each description, so vague descriptions mean missed delegation.
💡 Version note: The tool shows up as Agent in tool-use blocks. Older versions called it Task, and the init message's tool list still does, so match both names when you detect subagent calls in the message stream.
Block Risky Calls with Hooks
Hooks are callbacks that run at fixed points in the agent loop. The most useful one is PreToolUse, which fires before a tool runs and can deny it. A matcher filters by tool name, for example Bash or Write|Edit. Return {} to allow the call.
This Python hook refuses any shell command containing a recursive delete:
import asyncio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, HookMatcher
async def block_destructive_bash(input_data, tool_use_id, context):
command = input_data["tool_input"].get("command", "")
if "rm -rf" in command:
return {
"hookSpecificOutput": {
"hookEventName": input_data["hook_event_name"],
"permissionDecision": "deny",
"permissionDecisionReason": "Recursive deletes are blocked",
}
}
return {}
async def main():
options = ClaudeAgentOptions(
allowed_tools=["Bash", "Read"],
hooks={
"PreToolUse": [HookMatcher(matcher="Bash", hooks=[block_destructive_bash])]
},
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Clean up the build folder")
async for message in client.receive_response():
print(message)
asyncio.run(main())
And the same guard in TypeScript:
import { query, HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
const blockDestructiveBash: HookCallback = async (input) => {
const pre = input as PreToolUseHookInput;
const toolInput = pre.tool_input as Record<string, unknown>;
const command = String(toolInput?.command ?? "");
if (command.includes("rm -rf")) {
return {
hookSpecificOutput: {
hookEventName: pre.hook_event_name,
permissionDecision: "deny",
permissionDecisionReason: "Recursive deletes are blocked"
}
};
}
return {};
};
for await (const message of query({
prompt: "Clean up the build folder",
options: {
allowedTools: ["Bash", "Read"],
hooks: { PreToolUse: [{ matcher: "Bash", hooks: [blockDestructiveBash] }] }
}
})) {
if (message.type === "result") console.log(message.subtype);
}
When several hooks match, they run in parallel and the strictest answer wins. A single deny blocks the call no matter what the others return, so write each hook to work on its own.
Cap Turns and Spend
Permission modes set the default level of trust. Pick one with permission_mode or permissionMode:
Mode
Behavior
default
Standard behavior, unapproved tools go through the permission flow
acceptEdits
File edits are approved automatically
plan
Planning only: the agent researches without editing
dontAsk
Anything not pre-approved is denied
bypassPermissions
Permission checks are skipped, use with great care
auto
A model classifier reviews each action
Two numeric limits protect your wallet. max_turns (maxTurns) caps the agentic turns, and max_budget_usd (maxBudgetUsd) caps estimated spend. Hitting them ends the query with the result subtypes error_max_turns or error_max_budget_usd. Subagent requests count toward the same total_cost_usd, so a prompt that fans out into many subagents is still bound by the same cap.
💡 Tip for unattended jobs: Combine dontAsk with an explicit allowed-tools list. Anything outside the list is denied immediately instead of waiting for a human who is not there.
Three mistakes show up again and again in first agents:
Granting Bash too early. Start with read-only tools and add Edit or Bash only when the task demands it.
Vague subagent descriptions. Claude delegates based on the description text, so "helper agent" gets ignored while "Reviews code for security problems" gets used.
No limits on a first run. Set max_turns and max_budget_usd before you point the agent at a large repository.
Keep Context Across Sessions
Each query() call starts a new session. When a follow-up should remember what the agent already read and decided, you need a way to carry the session forward.
Multi-Turn in Python
ClaudeSDKClient tracks the session for you. Every client.query() continues the same conversation:
import asyncio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
async def main():
options = ClaudeAgentOptions(allowed_tools=["Read", "Edit", "Glob", "Grep"])
async with ClaudeSDKClient(options=options) as client:
await client.query("Inspect the auth module")
async for message in client.receive_response():
print(message)
# Same session, so the agent remembers the first answer
await client.query("Now refactor it to use JWT")
async for message in client.receive_response():
print(message)
asyncio.run(main())
Resume in TypeScript
TypeScript has no client object, so you capture the session_id from the result message and pass it back through resume:
import { query } from "@anthropic-ai/claude-agent-sdk";
let sessionId: string | undefined;
for await (const message of query({
prompt: "Inspect the auth module",
options: { allowedTools: ["Read", "Glob", "Grep"] }
})) {
if (message.type === "result") sessionId = message.session_id;
}
for await (const message of query({
prompt: "Now propose a refactor based on what you found",
options: { resume: sessionId, allowedTools: ["Read", "Glob", "Grep"] }
})) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}
Python accepts the same idea as resume=session_id. Both languages also support continue_conversation=True / continue: true to pick up the most recent session in the folder, and fork_session=True / forkSession: true to branch a copy of the history so you can try a different approach without losing the original.
Two limits matter in practice. Sessions store the conversation, not your files, so a forked agent that edits code changes the real files. And session files live on the machine that created them, so resuming on a different host needs a session store adapter or a copied transcript.
The Agent SDK runs on your machine with your own credentials, so PicassoIA does not replace it. Where PicassoIA helps is the drafting stage. The wording of a system prompt, a subagent description or a tool description decides how well an agent behaves, and testing wording in a chat form is faster than rerunning a script ten times.
Draft Prompts Before You Code
Claude Sonnet 5 is built for multi-step coding and tool-use tasks, which makes it a fair stand-in for rehearsing agent instructions. Follow these steps:
In the SDK itself, the model field takes the aliases described earlier, so you can run a cheap model for routine subagents and a stronger one for the main thread.
Create Your Own Images with PicassoIA
You now have a working agent, plus the habit of testing each piece before it ships. The same habit makes image generation better, and PicassoIA is a quick place to practice it.
Open Seedream 4.5 or GPT Image 2 and write a prompt with the same structure used for the photos in this article: the subject and its action, the setting, the light direction, the lens, and one or two surface textures. A line like "hands typing at a wooden desk, soft window light from the right, 85mm lens, shallow depth of field, visible dust on the wood grain" gives the model far more to work with than "a programmer typing".
Try it on the next blog header, product shot or documentation banner you need. Change one detail per run, compare the results side by side, and keep the prompt that works. When you want to see what else is available, browse every model at picassoia.com/en/all-models and start your first generation today.