claude-agents-sdk

Crates.ioclaude-agents-sdk
lib.rsclaude-agents-sdk
version0.1.3
created_at2026-01-02 11:51:03.144175+00
updated_at2026-01-03 20:58:28.94837+00
descriptionRust SDK for building agents with Claude Code CLI
homepage
repositoryhttps://github.com/jimmystridh/claude-agents-sdk
max_upload_size
id2018432
size642,317
Jimmy Stridh (jimmystridh)

documentation

README

claude-agents-sdk

A Rust SDK for building agents that interact with the Claude Code CLI.

Inspired by the Python Claude Agent SDK, providing similar functionality with idiomatic Rust APIs.

Features

  • Simple Query API: One-shot queries with query() function
  • Streaming Client: Full bidirectional ClaudeClient for complex interactions
  • Tool Permissions: Control which tools Claude can use with callbacks
  • Hooks: Register callbacks for various lifecycle events
  • MCP Tools: Define custom tools that run in-process
  • Type Safety: Strongly-typed messages, content blocks, and options
  • Async/Await: Built on Tokio for efficient async operations

Installation

Add to your Cargo.toml:

[dependencies]
claude-agents-sdk = "0.1"
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1"

For MCP tool support:

[dependencies]
claude-agents-sdk = { version = "0.1", features = ["mcp"] }

Prerequisites

Quick Start

Simple Query

use claude_agents_sdk::{query, ClaudeAgentOptions, Message};
use tokio_stream::StreamExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let options = ClaudeAgentOptions::new()
        .with_max_turns(3);

    let mut stream = query("What is 2 + 2?", Some(options), None).await?;

    while let Some(message) = stream.next().await {
        match message? {
            Message::Assistant(msg) => print!("{}", msg.text()),
            Message::Result(result) => {
                println!("\nCost: ${:.4}", result.total_cost_usd.unwrap_or(0.0));
            }
            _ => {}
        }
    }

    Ok(())
}

Streaming Client

use claude_agents_sdk::{ClaudeClient, ClaudeAgentOptions};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = ClaudeClient::new(None, None);
    client.connect().await?;

    // First query
    client.query("What is the capital of France?").await?;
    let (response, _) = client.receive_response().await?;
    println!("Response: {}", response);

    // Follow-up query
    client.query("What's its population?").await?;
    let (response, _) = client.receive_response().await?;
    println!("Response: {}", response);

    client.disconnect().await?;
    Ok(())
}

With Tool Permissions

use claude_agents_sdk::{ClaudeClientBuilder, PermissionResult, PermissionMode};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = ClaudeClientBuilder::new()
        .permission_mode(PermissionMode::Default)
        .can_use_tool(|tool_name, input, _ctx| async move {
            println!("Tool requested: {} with {:?}", tool_name, input);

            // Allow Read, deny dangerous Bash commands
            if tool_name == "Bash" {
                if let Some(cmd) = input.get("command").and_then(|v| v.as_str()) {
                    if cmd.contains("rm -rf") {
                        return PermissionResult::deny_with_message("Dangerous command");
                    }
                }
            }

            PermissionResult::allow()
        })
        .build();

    client.connect().await?;
    // ... use client
    Ok(())
}

API Reference

Entry Points

  • query(prompt, options, transport) - One-shot query returning a message stream
  • query_all(prompt, options) - Collect all messages from a query
  • query_result(prompt, options) - Get final response and result metadata

ClaudeClient

let mut client = ClaudeClient::new(options, transport);
client.connect().await?;
client.query("Hello").await?;
let (response, result) = client.receive_response().await?;
client.disconnect().await?;

Methods:

  • connect() - Connect to CLI
  • query(prompt) - Send a query
  • receive_messages() - Stream of messages
  • receive_response() - Collect response and result
  • interrupt() - Interrupt current operation
  • set_permission_mode(mode) - Change permission mode
  • set_model(model) - Change model
  • rewind_files(message_id) - Rewind to checkpoint
  • disconnect() - Disconnect from CLI

ClaudeAgentOptions

let options = ClaudeAgentOptions::new()
    .with_model("claude-3-sonnet")
    .with_system_prompt("You are helpful.")
    .with_max_turns(10)
    .with_permission_mode(PermissionMode::AcceptEdits)
    .with_allowed_tools(vec!["Bash".into(), "Read".into()])
    .with_partial_messages();

Message Types

enum Message {
    User(UserMessage),
    Assistant(AssistantMessage),
    System(SystemMessage),
    Result(ResultMessage),
    StreamEvent(StreamEvent),
}

// AssistantMessage has helpful methods
let text = assistant_msg.text();
let tool_uses = assistant_msg.tool_uses();

Content Blocks

enum ContentBlock {
    Text(TextBlock),
    Thinking(ThinkingBlock),
    ToolUse(ToolUseBlock),
    ToolResult(ToolResultBlock),
}

Examples

Run the examples:

# Simple one-shot query
cargo run --example simple_query

# Streaming client with multiple queries
cargo run --example streaming_client

# Tool permission callbacks
cargo run --example tool_permission_callback

# Lifecycle hooks (PreToolUse, PostToolUse)
cargo run --example hooks

# Error handling patterns
cargo run --example error_handling

# Streaming with progress indicators
cargo run --example streaming_progress

# System prompt configuration
cargo run --example system_prompt

# Budget limits
cargo run --example max_budget_usd

# MCP tools (requires --features mcp)
cargo run --example mcp_calculator --features mcp

See examples/ directory for the full list.

Testing

Unit Tests

Unit tests run without authentication:

cargo test
# or
make test

Integration Tests

Integration tests run against the real Claude API in Docker. They require authentication.

Setup (one-time):

# Generate an OAuth token (requires Claude Pro/Max subscription)
claude setup-token

# Create .env file with your token
cp .env.example .env
# Edit .env and paste your token

Run tests:

# Run all integration tests
make integration-test

# Run with verbose output
make integration-test-verbose

# Run specific test
./scripts/run-integration-tests.sh test_oneshot

# Interactive shell for debugging
make integration-shell

What's tested:

  • Core query functionality (one-shot and streaming)
  • Multi-turn conversations with context
  • Tool permission callbacks
  • Cancellation and timeout handling
  • Resource cleanup (no process leaks)
  • Error handling and edge cases

See TESTING.md for the complete testing guide.

Error Handling

The SDK provides ClaudeSDKError, a comprehensive error type for all failure modes.

Error Types

use claude_agents_sdk::ClaudeSDKError;

match result {
    // CLI not found or not installed
    Err(ClaudeSDKError::CLINotFound { message }) => {
        eprintln!("Claude CLI not installed: {}", message);
        eprintln!("Install from: https://docs.anthropic.com/en/docs/claude-code");
    }

    // CLI process exited with error
    Err(ClaudeSDKError::Process { exit_code, stderr, .. }) => {
        eprintln!("CLI failed with exit code {:?}", exit_code);
        if let Some(err) = stderr {
            eprintln!("stderr: {}", err);
        }
    }

    // Operation timed out
    Err(ClaudeSDKError::Timeout { duration_ms }) => {
        eprintln!("Operation timed out after {}ms", duration_ms);
    }

    // Connection to CLI failed
    Err(ClaudeSDKError::CLIConnection { message }) => {
        eprintln!("Failed to connect to CLI: {}", message);
    }

    // Invalid configuration
    Err(ClaudeSDKError::Configuration { message }) => {
        eprintln!("Configuration error: {}", message);
    }

    // Message parsing failed (malformed JSON from CLI)
    Err(ClaudeSDKError::MessageParse { message, .. }) => {
        eprintln!("Failed to parse CLI message: {}", message);
    }

    // Control protocol error
    Err(ClaudeSDKError::ControlProtocol { message, .. }) => {
        eprintln!("Control protocol error: {}", message);
    }

    // All other errors
    Err(e) => eprintln!("Error: {}", e),

    Ok(_) => {}
}

Recoverable Errors

Some errors can be retried:

if error.is_recoverable() {
    // Can retry: CLIConnection, Timeout, Channel errors
    tokio::time::sleep(Duration::from_secs(1)).await;
    // ... retry operation
}

Timeout Configuration

Configure timeouts to prevent indefinite hangs:

let options = ClaudeAgentOptions::new()
    .with_timeout_secs(60)  // 60 second timeout
    .with_max_turns(5);

// Or disable timeout (not recommended)
let options = ClaudeAgentOptions::new()
    .with_timeout_secs(0);  // No timeout

Default timeout is 300 seconds (5 minutes).

Stream Error Handling

When consuming message streams, handle errors per-message:

while let Some(result) = stream.next().await {
    match result {
        Ok(Message::Assistant(msg)) => {
            println!("{}", msg.text());
        }
        Ok(Message::Result(result)) => {
            if result.is_error {
                eprintln!("Query failed: {:?}", result.result);
            }
        }
        Err(e) => {
            eprintln!("Stream error: {}", e);
            break;
        }
        _ => {}
    }
}

Result Type

All SDK functions return Result<T, ClaudeSDKError>:

use claude_agents_sdk::Result;

async fn my_function() -> Result<String> {
    let mut client = ClaudeClient::new(None, None);
    client.connect().await?;  // Propagates errors with ?
    // ...
    Ok("success".to_string())
}

Architecture

The SDK communicates with the Claude Code CLI via a subprocess:

┌─────────────────┐     stdin/stdout      ┌──────────────┐
│  Rust SDK       │ ◄──── JSON-RPC ────► │  Claude CLI  │
│  (your code)    │                       │  (subprocess)│
└─────────────────┘                       └──────────────┘

Key internal components:

  • Transport - Abstract communication layer
  • SubprocessTransport - Subprocess implementation
  • Query - Control protocol handler
  • InternalClient - Core query processing

Comparison with Python SDK

Feature Python Rust
One-shot query query() query()
Streaming client ClaudeSDKClient ClaudeClient
Tool callback can_use_tool can_use_tool
Hooks hooks dict hooks HashMap
Async asyncio tokio
Type safety TypedDict Enums + Structs

License

MIT License - see LICENSE for details.

Commit count: 0

cargo fmt