No description
Find a file
Claude 5724537b20
docs: add Tool Permission Callbacks section to README
Add documentation for the can_use_tool callback mechanism that allows
programmatic control over which tools Claude can use at runtime. This
addresses issue #137 by making the feature more discoverable.

Includes:
- Basic example showing allow/deny patterns
- Example of modifying tool inputs before execution
- Comparison table: can_use_tool vs PreToolUse hooks
- Reference to the comprehensive example file
2025-12-17 12:26:46 +00:00
.claude Add Docker-based test infrastructure for e2e tests (#424) 2025-12-16 10:53:13 -08:00
.github/workflows Add Docker-based test infrastructure for e2e tests (#424) 2025-12-16 10:53:13 -08:00
e2e-tests Add Docker-based test infrastructure for e2e tests (#424) 2025-12-16 10:53:13 -08:00
examples Add Docker-based test infrastructure for e2e tests (#424) 2025-12-16 10:53:13 -08:00
scripts Add Docker-based test infrastructure for e2e tests (#424) 2025-12-16 10:53:13 -08:00
src/claude_agent_sdk chore: bump bundled CLI version to 2.0.71 2025-12-16 22:09:36 +00:00
tests Add UUID to UserMessage response type to improve devX for rewind (#418) 2025-12-15 09:03:58 -08:00
.dockerignore Add Docker-based test infrastructure for e2e tests (#424) 2025-12-16 10:53:13 -08:00
.gitignore feat: add max_thinking_tokens option to ClaudeAgentOptions (#298) 2025-10-30 15:14:42 -07:00
CHANGELOG.md chore: release v0.1.17 (#419) 2025-12-15 16:41:49 -08:00
CLAUDE.md Rename claude_code to claude_agent (#188) 2025-09-28 14:52:53 -07:00
Dockerfile.test Add Docker-based test infrastructure for e2e tests (#424) 2025-12-16 10:53:13 -08:00
LICENSE Initial Python SDK import 2025-06-12 00:16:19 -07:00
pyproject.toml chore: release v0.1.17 (#419) 2025-12-15 16:41:49 -08:00
README.md docs: add Tool Permission Callbacks section to README 2025-12-17 12:26:46 +00:00

Claude Agent SDK for Python

Python SDK for Claude Agent. See the Claude Agent SDK documentation for more information.

Installation

pip install claude-agent-sdk

Prerequisites:

  • Python 3.10+

Note: The Claude Code CLI is automatically bundled with the package - no separate installation required! The SDK will use the bundled CLI by default. If you prefer to use a system-wide installation or a specific version, you can:

  • Install Claude Code separately: curl -fsSL https://claude.ai/install.sh | bash
  • Specify a custom path: ClaudeAgentOptions(cli_path="/path/to/claude")

Quick Start

import anyio
from claude_agent_sdk import query

async def main():
    async for message in query(prompt="What is 2 + 2?"):
        print(message)

anyio.run(main)

Basic Usage: query()

query() is an async function for querying Claude Code. It returns an AsyncIterator of response messages. See src/claude_agent_sdk/query.py.

from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, TextBlock

# Simple query
async for message in query(prompt="Hello Claude"):
    if isinstance(message, AssistantMessage):
        for block in message.content:
            if isinstance(block, TextBlock):
                print(block.text)

# With options
options = ClaudeAgentOptions(
    system_prompt="You are a helpful assistant",
    max_turns=1
)

async for message in query(prompt="Tell me a joke", options=options):
    print(message)

Using Tools

options = ClaudeAgentOptions(
    allowed_tools=["Read", "Write", "Bash"],
    permission_mode='acceptEdits'  # auto-accept file edits
)

async for message in query(
    prompt="Create a hello.py file",
    options=options
):
    # Process tool use and results
    pass

Working Directory

from pathlib import Path

options = ClaudeAgentOptions(
    cwd="/path/to/project"  # or Path("/path/to/project")
)

ClaudeSDKClient

ClaudeSDKClient supports bidirectional, interactive conversations with Claude Code. See src/claude_agent_sdk/client.py.

Unlike query(), ClaudeSDKClient additionally enables custom tools and hooks, both of which can be defined as Python functions.

Custom Tools (as In-Process SDK MCP Servers)

A custom tool is a Python function that you can offer to Claude, for Claude to invoke as needed.

Custom tools are implemented in-process MCP servers that run directly within your Python application, eliminating the need for separate processes that regular MCP servers require.

For an end-to-end example, see MCP Calculator.

Creating a Simple Tool

from claude_agent_sdk import tool, create_sdk_mcp_server, ClaudeAgentOptions, ClaudeSDKClient

# Define a tool using the @tool decorator
@tool("greet", "Greet a user", {"name": str})
async def greet_user(args):
    return {
        "content": [
            {"type": "text", "text": f"Hello, {args['name']}!"}
        ]
    }

# Create an SDK MCP server
server = create_sdk_mcp_server(
    name="my-tools",
    version="1.0.0",
    tools=[greet_user]
)

# Use it with Claude
options = ClaudeAgentOptions(
    mcp_servers={"tools": server},
    allowed_tools=["mcp__tools__greet"]
)

async with ClaudeSDKClient(options=options) as client:
    await client.query("Greet Alice")

    # Extract and print response
    async for msg in client.receive_response():
        print(msg)

Benefits Over External MCP Servers

  • No subprocess management - Runs in the same process as your application
  • Better performance - No IPC overhead for tool calls
  • Simpler deployment - Single Python process instead of multiple
  • Easier debugging - All code runs in the same process
  • Type safety - Direct Python function calls with type hints

Migration from External Servers

# BEFORE: External MCP server (separate process)
options = ClaudeAgentOptions(
    mcp_servers={
        "calculator": {
            "type": "stdio",
            "command": "python",
            "args": ["-m", "calculator_server"]
        }
    }
)

# AFTER: SDK MCP server (in-process)
from my_tools import add, subtract  # Your tool functions

calculator = create_sdk_mcp_server(
    name="calculator",
    tools=[add, subtract]
)

options = ClaudeAgentOptions(
    mcp_servers={"calculator": calculator}
)

Mixed Server Support

You can use both SDK and external MCP servers together:

options = ClaudeAgentOptions(
    mcp_servers={
        "internal": sdk_server,      # In-process SDK server
        "external": {                # External subprocess server
            "type": "stdio",
            "command": "external-server"
        }
    }
)

Hooks

A hook is a Python function that the Claude Code application (not Claude) invokes at specific points of the Claude agent loop. Hooks can provide deterministic processing and automated feedback for Claude. Read more in Claude Code Hooks Reference.

For more examples, see examples/hooks.py.

Example

from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, HookMatcher

async def check_bash_command(input_data, tool_use_id, context):
    tool_name = input_data["tool_name"]
    tool_input = input_data["tool_input"]
    if tool_name != "Bash":
        return {}
    command = tool_input.get("command", "")
    block_patterns = ["foo.sh"]
    for pattern in block_patterns:
        if pattern in command:
            return {
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "deny",
                    "permissionDecisionReason": f"Command contains invalid pattern: {pattern}",
                }
            }
    return {}

options = ClaudeAgentOptions(
    allowed_tools=["Bash"],
    hooks={
        "PreToolUse": [
            HookMatcher(matcher="Bash", hooks=[check_bash_command]),
        ],
    }
)

async with ClaudeSDKClient(options=options) as client:
    # Test 1: Command with forbidden pattern (will be blocked)
    await client.query("Run the bash command: ./foo.sh --help")
    async for msg in client.receive_response():
        print(msg)

    print("\n" + "=" * 50 + "\n")

    # Test 2: Safe command that should work
    await client.query("Run the bash command: echo 'Hello from hooks example!'")
    async for msg in client.receive_response():
        print(msg)

Tool Permission Callbacks

A tool permission callback (can_use_tool) lets you programmatically control which tools Claude can use at runtime. Unlike hooks which run after permission is granted, can_use_tool intercepts tool requests before execution, allowing you to:

  • Allow or deny specific tools based on their inputs
  • Modify tool inputs before execution (e.g., redirect file paths)
  • Implement custom security policies
  • Log tool usage for auditing

Important: Without a can_use_tool callback, the SDK assumes "Denied" by default for operations outside the allowed working directory.

Basic Example

from claude_agent_sdk import (
    ClaudeAgentOptions,
    ClaudeSDKClient,
    PermissionResultAllow,
    PermissionResultDeny,
    ToolPermissionContext,
)

async def my_permission_callback(
    tool_name: str,
    input_data: dict,
    context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
    """Control tool permissions based on tool type and input."""

    # Always allow read-only operations
    if tool_name in ["Read", "Glob", "Grep"]:
        return PermissionResultAllow()

    # Deny writes to system directories
    if tool_name in ["Write", "Edit"]:
        file_path = input_data.get("file_path", "")
        if file_path.startswith("/etc/") or file_path.startswith("/usr/"):
            return PermissionResultDeny(
                message=f"Cannot write to system directory: {file_path}"
            )

    # Block dangerous bash commands
    if tool_name == "Bash":
        command = input_data.get("command", "")
        if "rm -rf" in command or "sudo" in command:
            return PermissionResultDeny(
                message="Dangerous command pattern detected"
            )

    # Allow everything else
    return PermissionResultAllow()

# Use the callback
options = ClaudeAgentOptions(
    can_use_tool=my_permission_callback,
    permission_mode="default"
)

async with ClaudeSDKClient(options) as client:
    await client.query("List files in /var/log")
    async for msg in client.receive_response():
        print(msg)

Modifying Tool Inputs

You can also modify tool inputs before execution:

async def redirect_writes(
    tool_name: str,
    input_data: dict,
    context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
    if tool_name == "Write":
        # Redirect all writes to a safe directory
        modified_input = input_data.copy()
        original_path = input_data.get("file_path", "")
        modified_input["file_path"] = f"./safe_output/{original_path.split('/')[-1]}"
        return PermissionResultAllow(updated_input=modified_input)
    return PermissionResultAllow()

When to Use Tool Permission Callbacks vs Hooks

Feature can_use_tool Callback PreToolUse Hook
Timing Before permission is granted After tool is approved
Can deny execution Yes Yes (via permissionDecision)
Can modify inputs Yes (updated_input) No
Use case Security policies, input sanitization Logging, validation, side effects

For a complete example with logging and interactive prompts, see examples/tool_permission_callback.py.

Types

See src/claude_agent_sdk/types.py for complete type definitions:

  • ClaudeAgentOptions - Configuration options
  • AssistantMessage, UserMessage, SystemMessage, ResultMessage - Message types
  • TextBlock, ToolUseBlock, ToolResultBlock - Content blocks

Error Handling

from claude_agent_sdk import (
    ClaudeSDKError,      # Base error
    CLINotFoundError,    # Claude Code not installed
    CLIConnectionError,  # Connection issues
    ProcessError,        # Process failed
    CLIJSONDecodeError,  # JSON parsing issues
)

try:
    async for message in query(prompt="Hello"):
        pass
except CLINotFoundError:
    print("Please install Claude Code")
except ProcessError as e:
    print(f"Process failed with exit code: {e.exit_code}")
except CLIJSONDecodeError as e:
    print(f"Failed to parse response: {e}")

See src/claude_agent_sdk/_errors.py for all error types.

Available Tools

See the Claude Code documentation for a complete list of available tools.

Examples

See examples/quick_start.py for a complete working example.

See examples/streaming_mode.py for comprehensive examples involving ClaudeSDKClient. You can even run interactive examples in IPython from examples/streaming_mode_ipython.py.

Migrating from Claude Code SDK

If you're upgrading from the Claude Code SDK (versions < 0.1.0), please see the CHANGELOG.md for details on breaking changes and new features, including:

  • ClaudeCodeOptionsClaudeAgentOptions rename
  • Merged system prompt configuration
  • Settings isolation and explicit control
  • New programmatic subagents and session forking features

Development

If you're contributing to this project, run the initial setup script to install git hooks:

./scripts/initial-setup.sh

This installs a pre-push hook that runs lint checks before pushing, matching the CI workflow. To skip the hook temporarily, use git push --no-verify.

Building Wheels Locally

To build wheels with the bundled Claude Code CLI:

# Install build dependencies
pip install build twine

# Build wheel with bundled CLI
python scripts/build_wheel.py

# Build with specific version
python scripts/build_wheel.py --version 0.1.4

# Build with specific CLI version
python scripts/build_wheel.py --cli-version 2.0.0

# Clean bundled CLI after building
python scripts/build_wheel.py --clean

# Skip CLI download (use existing)
python scripts/build_wheel.py --skip-download

The build script:

  1. Downloads Claude Code CLI for your platform
  2. Bundles it in the wheel
  3. Builds both wheel and source distribution
  4. Checks the package with twine

See python scripts/build_wheel.py --help for all options.

Release Workflow

The package is published to PyPI via the GitHub Actions workflow in .github/workflows/publish.yml. To create a new release:

  1. Trigger the workflow manually from the Actions tab with two inputs:

    • version: The package version to publish (e.g., 0.1.5)
    • claude_code_version: The Claude Code CLI version to bundle (e.g., 2.0.0 or latest)
  2. The workflow will:

    • Build platform-specific wheels for macOS, Linux, and Windows
    • Bundle the specified Claude Code CLI version in each wheel
    • Build a source distribution
    • Publish all artifacts to PyPI
    • Create a release branch with version updates
    • Open a PR to main with:
      • Updated pyproject.toml version
      • Updated src/claude_agent_sdk/_version.py
      • Updated src/claude_agent_sdk/_cli_version.py with bundled CLI version
      • Auto-generated CHANGELOG.md entry
  3. Review and merge the release PR to update main with the new version information

The workflow tracks both the package version and the bundled CLI version separately, allowing you to release a new package version with an updated CLI without code changes.

License and terms

Use of this SDK is governed by Anthropic's Commercial Terms of Service, including when you use it to power products and services that you make available to your own customers and end users, except to the extent a specific component or dependency is covered by a different license as indicated in that component's LICENSE file.