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.

Claude Agent SDK Tutorial: Python and TypeScript Examples
Cristian Da Conceicao
Founder of Picasso IA

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 toUseWhat you get
Embed an agent in your own Python or TypeScript appAgent SDKThe Claude Code agent loop as a library, with built-in tools, permissions, sessions and hooks
Work interactively from a terminalClaude Code CLIA terminal interface built for daily use and one-off tasks
Call the Claude API from your own codeClient SDKDirect API access where you write the tool loop yourself
Let Anthropic host the agentManaged AgentsA 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.

Close-up of a developer's hands typing at a desk in soft window light

Install and Authenticate

Install the Package

For Python, create a virtual environment and install the package:

python3 -m venv .venv
source .venv/bin/activate
pip install claude-agent-sdk

On Windows PowerShell, the activation line is .venv\Scripts\Activate.ps1. For TypeScript, start a project and add the package plus tsx:

npm init -y
npm pkg set type=module
npm install @anthropic-ai/claude-agent-sdk
npm install --save-dev tsx

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.

A developer working at a standing desk in a bright loft office

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

A laptop on a marble cafe table photographed from a low angle

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.

SettingPythonTypeScript
Pre-approved toolsallowed_toolsallowedTools
Permission modepermission_modepermissionMode
Turn limitmax_turnsmaxTurns
Spend limitmax_budget_usdmaxBudgetUsd
Custom tool serversmcp_serversmcpServers
Resume a sessionresumeresume
Subagentsagentsagents

Tool lists are how you dial autonomy up or down:

ToolsWhat the agent can do
Read, Glob, GrepLook at code and change nothing
Read, Edit, GlobLook at code and modify it
Read, Edit, Bash, Glob, GrepRun end to end, including shell commands

A top-down view of a wooden desk with a laptop and a notebook of hand-drawn diagrams

💡 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);
  }
}

A craftsperson's workbench with neatly arranged hand tools

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

Three colleagues collaborating around a table with a whiteboard of sticky notes

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.

A heavy brass padlock on a weathered wooden gate

Cap Turns and Spend

Permission modes set the default level of trust. Pick one with permission_mode or permissionMode:

ModeBehavior
defaultStandard behavior, unapproved tools go through the permission flow
acceptEditsFile edits are approved automatically
planPlanning only: the agent researches without editing
dontAskAnything not pre-approved is denied
bypassPermissionsPermission checks are skipped, use with great care
autoA 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.

An open notebook with a ribbon bookmark and a fountain pen in the fold

Use Claude Sonnet 5 on PicassoIA

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:

  1. Open the Claude Sonnet 5 page on PicassoIA.
  2. Paste your draft agent instructions into the System Prompt field.
  3. Type a realistic task in Prompt, for example the contents of utils.py plus "list every input that would crash it".
  4. Set effort. The default is low, which turns thinking off for the fastest, cheapest reply. Raise it to high or max for bugs that span several files.
  5. Leave max_tokens at the default of 8192, enough for detailed code or text in a single response.
  6. Optionally attach a screenshot of an error through the image input, since the model can read it.
  7. Run it, tighten the wording until the answer matches what you want, then copy the final text into system_prompt or a subagent prompt.

💡 Remember: This rehearses wording only. File tools, hooks and the agent loop still come from the SDK on your machine.

Pick the Right Claude Model

PicassoIA lists several Claude models, and each suits a different drafting job:

ModelBest for
Claude Sonnet 5Coding and tool-use tasks with adjustable effort
Claude Fable 5Complex coding tasks
Claude Opus 4.7Coding, image reading and reasoning in one model
Claude 4.5 HaikuFast text and code replies

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.

A developer leaning back in a chair with a relaxed smile beside an open laptop

Share this article