Skip to content

Latest commit

 

History

History
848 lines (646 loc) · 20.7 KB

API_GUIDE.md

File metadata and controls

848 lines (646 loc) · 20.7 KB

Claude Agent SDK for Rust - API Guide

Table of Contents

Overview

The Claude Agent SDK for Rust provides a production-ready interface to Claude Code CLI, enabling you to build AI agents with full control over permissions, hooks, and agent behavior.

Key Features

  • Simple Query API - One-shot queries with streaming responses
  • Interactive Client - Multi-turn conversations with bidirectional communication
  • Hook System - Intercept and control agent behavior at key points
  • Permission Callbacks - Fine-grained control over tool usage
  • Type-Safe - Fully typed API with compile-time safety
  • Async/Await - Built on Tokio for efficient async operations

Quick Start

use claude_agent_sdk_rust::{query, Message};
use futures::StreamExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Simple one-shot query
    let mut messages = query("What is 2 + 2?", None).await?;

    while let Some(msg) = messages.next().await {
        match msg? {
            Message::Assistant(assistant) => {
                println!("Claude: {:?}", assistant);
            }
            Message::Result(result) => {
                println!("Cost: ${:.4}", result.total_cost_usd.unwrap_or(0.0));
            }
            _ => {}
        }
    }

    Ok(())
}

Core Concepts

1. Query vs Client

query() - For simple, one-shot queries:

  • Spawns CLI, sends prompt, returns stream of messages
  • Automatically cleans up after result
  • Best for single questions or tasks

ClaudeSDKClient - For interactive sessions:

  • Maintains persistent connection
  • Supports multiple queries in sequence
  • Dynamic configuration changes
  • Full control protocol access

2. Message Stream

All interactions return a stream of Message enum variants:

pub enum Message {
    User(UserMessage),        // User input
    Assistant(AssistantMessage), // Claude's response
    System(SystemMessage),    // System metadata
    Result(ResultMessage),    // Final metrics
    StreamEvent(StreamEvent), // Partial messages
}

3. Content Blocks

Assistant messages contain content blocks:

pub enum ContentBlock {
    Text(TextBlock),           // Text output
    Thinking(ThinkingBlock),   // Extended thinking
    ToolUse(ToolUseBlock),     // Tool invocation
    ToolResult(ToolResultBlock), // Tool output
}

Simple Query API

Basic Usage

use claude_agent_sdk_rust::query;
use futures::StreamExt;

let messages = query("Explain async/await in Rust", None).await?;
let mut messages = Box::pin(messages);

while let Some(msg) = messages.next().await {
    // Handle message
}

With Options

use claude_agent_sdk_rust::{query, ClaudeAgentOptions, PermissionMode};

let options = ClaudeAgentOptions::builder()
    .permission_mode(PermissionMode::AcceptEdits)
    .max_turns(5)
    .allowed_tools(vec!["Read".to_string(), "Write".to_string()])
    .build();

let messages = query("Help me refactor this code", Some(options)).await?;

Interactive Client API

Connection Lifecycle

use claude_agent_sdk_rust::{ClaudeSDKClient, ClaudeAgentOptions};

// Create client
let mut client = ClaudeSDKClient::new(ClaudeAgentOptions::default());

// Connect (starts CLI process)
client.connect(Some("Hello!".to_string())).await?;

// Use client for multiple queries...

// Disconnect (cleans up)
client.disconnect().await?;

Sending Queries

// Send a query
client.query("What files are in this directory?").await?;

// Receive response
let messages = client.receive_response()?;
let mut messages = Box::pin(messages);

while let Some(msg) = messages.next().await {
    match msg? {
        Message::Assistant(a) => { /* handle */ }
        Message::Result(r) => break, // Auto-terminates
        _ => {}
    }
}

Dynamic Configuration

// Change permission mode mid-session
client.set_permission_mode(PermissionMode::AcceptAll).await?;

// Change AI model
client.set_model(Some("claude-opus-4-20250514".to_string())).await?;

// Interrupt current processing
client.interrupt().await?;

Hook System

Hooks allow you to intercept and control agent behavior at specific points.

Hook Events

  • PreToolUse - Before tool execution
  • PostToolUse - After tool execution
  • PostToolUseFailure - After a tool execution fails
  • UserPromptSubmit - When user submits a prompt
  • Notification - When the agent sends a notification
  • Stop - When agent loop stops
  • SubagentStart - When a subagent starts
  • SubagentStop - When a subagent stops
  • PreCompact - Before context compaction
  • PermissionRequest - When a permission decision is needed

Implementing a Hook

use claude_agent_sdk_rust::callbacks::{HookCallback, hooks};
use claude_agent_sdk_rust::types::{HookInput, HookOutput, HookContext};
use async_trait::async_trait;

struct LoggingHook;

#[async_trait]
impl HookCallback for LoggingHook {
    async fn call(
        &self,
        input: HookInput,
        tool_use_id: Option<String>,
        _context: HookContext,
    ) -> claude_agent_sdk_rust::Result<HookOutput> {
        // Match on hook input type
        if let HookInput::PreToolUse(pre_tool) = input {
            println!("Tool: {}", pre_tool.tool_name);
            println!("Input: {:?}", pre_tool.tool_input);
        }

        // Allow execution to continue
        Ok(hooks::allow())
    }
}

Registering Hooks

use claude_agent_sdk_rust::types::HookEvent;

let mut client = ClaudeSDKClient::new(options);

// Register hook for all tools
let hook_id = client.register_hook(
    HookEvent::PreToolUse,
    None,  // Match all tools
    LoggingHook,
);

// Register hook for specific tools
let hook_id = client.register_hook(
    HookEvent::PreToolUse,
    Some("Bash|Edit"),  // Match Bash or Edit
    SecurityHook,
);

Hook Responses

use claude_agent_sdk_rust::callbacks::hooks;

// Allow execution
Ok(hooks::allow())

// Block execution
Ok(hooks::block("Reason for blocking"))

// Allow with message to user
Ok(hooks::allow_with_message("Custom message"))

// Defer (async execution)
Ok(hooks::defer(Some(5000))) // 5 second timeout

Permission Callbacks

Permission callbacks control which tools Claude can use and how.

Implementing a Permission Callback

use claude_agent_sdk_rust::callbacks::{PermissionCallback, permissions};
use claude_agent_sdk_rust::types::{PermissionResult, ToolPermissionContext};
use async_trait::async_trait;
use serde_json::Value;

struct SafetyChecker;

#[async_trait]
impl PermissionCallback for SafetyChecker {
    async fn call(
        &self,
        tool_name: String,
        input: Value,
        _context: ToolPermissionContext,
    ) -> claude_agent_sdk_rust::Result<PermissionResult> {
        // Block dangerous commands
        if tool_name == "Bash" {
            if let Some(cmd) = input.get("command").and_then(|v| v.as_str()) {
                if cmd.contains("rm -rf") {
                    return Ok(permissions::deny("Dangerous command blocked"));
                }
            }
        }

        // Allow by default
        Ok(permissions::allow())
    }
}

Registering Permission Callback

let mut client = ClaudeSDKClient::new(options);

// Set permission callback (only one active at a time)
client.set_permission_callback(SafetyChecker);

Permission Responses

use claude_agent_sdk_rust::callbacks::permissions;

// Allow tool use
Ok(permissions::allow())

// Allow with modified input
Ok(permissions::allow_with_input(modified_input))

// Deny tool use
Ok(permissions::deny("Not allowed"))

// Deny and stop session
Ok(permissions::deny_and_interrupt("Critical violation"))

Thinking and Effort

Control Claude's reasoning depth with thinking configuration and effort levels.

ThinkingConfig

use claude_agent_sdk_rust::{ClaudeAgentOptions, ThinkingConfig};

// Adaptive: Claude decides when to use extended thinking
let options = ClaudeAgentOptions::builder()
    .thinking(Some(ThinkingConfig::Adaptive))
    .build();

// Enabled with explicit token budget
let options = ClaudeAgentOptions::builder()
    .thinking(Some(ThinkingConfig::Enabled { budget_tokens: 10000 }))
    .build();

// Disabled
let options = ClaudeAgentOptions::builder()
    .thinking(Some(ThinkingConfig::Disabled))
    .build();

Maps to the --max-thinking-tokens CLI flag. Adaptive passes 0, Enabled passes the budget, Disabled omits the flag.

Effort

use claude_agent_sdk_rust::{ClaudeAgentOptions, Effort};

let options = ClaudeAgentOptions::builder()
    .effort(Some(Effort::High))
    .build();

Levels: Low, Medium, High, Max. Maps to the --effort CLI flag.

Structured Output

Get Claude's response as validated JSON conforming to a schema. The output_format option accepts a JSON value with type: "json_schema" and a schema field containing a standard JSON Schema.

Important: Structured output typically requires more than 1 turn. Set max_turns to at least 2-3 or omit it entirely. Using max_turns(1) will likely produce error_max_turns with no structured output.

use claude_agent_sdk_rust::{query, ClaudeAgentOptions, Message};
use futures::StreamExt;

let schema = serde_json::json!({
    "type": "json_schema",
    "schema": {
        "type": "object",
        "properties": {
            "summary": { "type": "string" },
            "language": { "type": "string" },
            "line_count": { "type": "number" }
        },
        "required": ["summary", "language", "line_count"]
    }
});

let options = ClaudeAgentOptions::builder()
    .output_format(Some(schema))
    .build();

let mut messages = query("Analyze src/main.rs", Some(options)).await?;
let mut messages = Box::pin(messages);

while let Some(msg) = messages.next().await {
    if let Ok(Message::Result(result)) = msg {
        if let Some(output) = &result.structured_output {
            // output is a serde_json::Value matching your schema
            println!("{}", serde_json::to_string_pretty(output)?);
        }
    }
}

The structured output is available on ResultMessage.structured_output when the query completes with subtype: "success".

Budget and Cost Control

max_budget_usd

Set a per-query spending limit:

let options = ClaudeAgentOptions::builder()
    .max_budget_usd(Some(0.10))
    .build();

Important: This is a soft cap. The budget is checked between turns — the current turn always completes before the limit is evaluated. Actual spend may slightly exceed the configured value.

Fallback Model

Specify a model to fall back to if the primary model is unavailable:

let options = ClaudeAgentOptions::builder()
    .model(Some("claude-opus-4-20250514".into()))
    .fallback_model(Some("claude-sonnet-4-5-20250929".into()))
    .build();

Configuration Options

ClaudeAgentOptions

use claude_agent_sdk_rust::{
    ClaudeAgentOptions, PermissionMode, SystemPrompt, SettingSource,
    ThinkingConfig, Effort,
};

let options = ClaudeAgentOptions::builder()
    // Permission mode
    .permission_mode(PermissionMode::AcceptEdits)

    // Allowed tools (whitelist)
    .allowed_tools(vec!["Read".into(), "Write".into()])

    // Blocked tools (blacklist)
    .blocked_tools(vec!["Bash".into()])

    // System prompt
    .system_prompt(SystemPrompt::Text(
        "You are a Rust expert assistant".into()
    ))

    // Model selection
    .model(Some("claude-sonnet-4-5-20250929".into()))
    .fallback_model(Some("claude-haiku-4-5-20251001".into()))

    // Conversation limits
    .max_turns(10u32)
    .max_budget_usd(Some(0.50))

    // Thinking and effort
    .thinking(Some(ThinkingConfig::Adaptive))
    .effort(Some(Effort::High))

    // Structured output
    .output_format(Some(serde_json::json!({
        "type": "json_schema",
        "schema": { "type": "object", "properties": {} }
    })))

    // CLI path (auto-detected if not set)
    .cli_path(Some("/path/to/claude".into()))

    // Settings sources
    .setting_sources(Some(vec![
        SettingSource::User,
        SettingSource::Project,
    ]))

    .build();

Permission Modes

pub enum PermissionMode {
    AcceptAll,    // Auto-approve all tools
    AcceptEdits,  // Auto-approve file edits only
    Ask,          // Prompt for each tool (default)
    Callback,     // Use permission callback
}

Message Types

AssistantMessage

pub struct AssistantMessage {
    pub message: AssistantMessageInner,
    pub parent_tool_use_id: Option<String>,
}

pub struct AssistantMessageInner {
    pub role: String,  // "assistant"
    pub content: Vec<ContentBlock>,
}

ResultMessage

pub struct ResultMessage {
    pub subtype: String,                        // "success" | "error_max_turns" | ...
    pub duration_ms: u64,
    pub duration_api_ms: u64,
    pub is_error: bool,
    pub num_turns: u32,
    pub session_id: String,
    pub total_cost_usd: Option<f64>,
    pub usage: Option<Value>,
    pub result: Option<String>,                 // Result text (if any)
    pub structured_output: Option<Value>,       // JSON output when output_format is set
}

SystemMessage

pub struct SystemMessage {
    pub subtype: String,
    pub content: Option<Value>,
}

Error Handling

Error Types

pub enum ClaudeSDKError {
    CLINotFound(String),
    TransportError(String),
    MessageParse(String),
    NotConnected,
    AlreadyConnected,
    HookNotFound(String),
    PermissionCallbackNotSet,
    ControlTimeout { timeout_seconds: u64, request: String },
    Other(String),
}

Handling Errors

use claude_agent_sdk_rust::ClaudeSDKError;

match query("prompt", None).await {
    Ok(stream) => { /* handle stream */ }
    Err(ClaudeSDKError::CLINotFound(msg)) => {
        eprintln!("Claude CLI not found: {}", msg);
        eprintln!("Install from: https://claude.com/claude-code");
    }
    Err(e) => eprintln!("Error: {}", e),
}

Examples

Example 1: Simple Code Review

use claude_agent_sdk_rust::{query, Message, ClaudeAgentOptions};
use futures::StreamExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let options = ClaudeAgentOptions::builder()
        .allowed_tools(vec!["Read".into()])
        .system_prompt(SystemPrompt::Text(
            "You are a code reviewer. Focus on best practices.".into()
        ))
        .build();

    let messages = query(
        "Review the code in src/main.rs",
        Some(options)
    ).await?;

    let mut messages = Box::pin(messages);

    while let Some(msg) = messages.next().await {
        if let Message::Assistant(a) = msg? {
            for block in &a.message.content {
                if let Some(text) = block.as_text() {
                    println!("{}", text.text);
                }
            }
        }
    }

    Ok(())
}

Example 2: Interactive Refactoring

use claude_agent_sdk_rust::{ClaudeSDKClient, ClaudeAgentOptions, Message};
use futures::StreamExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let options = ClaudeAgentOptions::builder()
        .permission_mode(PermissionMode::AcceptEdits)
        .build();

    let mut client = ClaudeSDKClient::new(options);
    client.connect(None).await?;

    // First query
    client.query("Refactor src/utils.rs to use modern Rust patterns").await?;
    let messages = client.receive_response()?;
    let mut messages = Box::pin(messages);

    while let Some(msg) = messages.next().await {
        if let Message::Result(_) = msg? { break; }
    }
    drop(messages);

    // Follow-up query
    client.query("Add tests for the refactored code").await?;
    let messages = client.receive_response()?;
    let mut messages = Box::pin(messages);

    while let Some(msg) = messages.next().await {
        if let Message::Result(_) = msg? { break; }
    }
    drop(messages);

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

Example 3: Safety-Enhanced Agent

use claude_agent_sdk_rust::{
    ClaudeSDKClient, ClaudeAgentOptions, Message,
    callbacks::{PermissionCallback, permissions, HookCallback, hooks},
    types::{HookEvent, HookInput, HookOutput, HookContext,
            PermissionResult, ToolPermissionContext},
};
use async_trait::async_trait;
use serde_json::Value;

struct CommandValidator;

#[async_trait]
impl PermissionCallback for CommandValidator {
    async fn call(
        &self,
        tool_name: String,
        input: Value,
        _ctx: ToolPermissionContext,
    ) -> claude_agent_sdk_rust::Result<PermissionResult> {
        if tool_name == "Bash" {
            if let Some(cmd) = input.get("command").and_then(|v| v.as_str()) {
                // Block network access
                if cmd.contains("curl") || cmd.contains("wget") {
                    return Ok(permissions::deny("Network access blocked"));
                }
            }
        }
        Ok(permissions::allow())
    }
}

struct AuditLogger;

#[async_trait]
impl HookCallback for AuditLogger {
    async fn call(
        &self,
        input: HookInput,
        _tool_use_id: Option<String>,
        _ctx: HookContext,
    ) -> claude_agent_sdk_rust::Result<HookOutput> {
        if let HookInput::PreToolUse(pre_tool) = input {
            // Log to audit file
            println!("[AUDIT] {} - {:?}", pre_tool.tool_name, pre_tool.tool_input);
        }
        Ok(hooks::allow())
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = ClaudeSDKClient::new(ClaudeAgentOptions::default());

    // Register security controls
    client.set_permission_callback(CommandValidator);
    client.register_hook(HookEvent::PreToolUse, None, AuditLogger);

    client.connect(None).await?;
    client.query("Analyze system logs").await?;

    // Process messages...

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

Best Practices

1. Stream Handling

Always pin streams before iterating:

let messages = client.receive_response()?;
let mut messages = Box::pin(messages);  // Pin the stream

while let Some(msg) = messages.next().await {
    // Handle messages
}

2. Resource Cleanup

Drop streams before reusing client:

let messages = client.receive_response()?;
let mut messages = Box::pin(messages);

// Process messages...

drop(messages);  // Drop stream before next query

client.query("Next question").await?;

3. Error Handling

Handle specific error types:

match client.connect(None).await {
    Ok(_) => { /* connected */ }
    Err(ClaudeSDKError::CLINotFound(_)) => {
        // Provide installation instructions
    }
    Err(ClaudeSDKError::AlreadyConnected) => {
        // Client is already connected
    }
    Err(e) => eprintln!("Connection error: {}", e),
}

4. Permission Callbacks

Always return a result - never panic:

#[async_trait]
impl PermissionCallback for MyChecker {
    async fn call(...) -> Result<PermissionResult> {
        // Validate inputs
        // Return allow/deny
        // Never panic!
        Ok(permissions::allow())
    }
}

5. Hook Callbacks

Keep hooks fast - avoid blocking operations:

#[async_trait]
impl HookCallback for FastHook {
    async fn call(...) -> Result<HookOutput> {
        // Quick validation/logging only
        // Defer heavy operations to async hooks
        Ok(hooks::allow())
    }
}

Troubleshooting

CLI Not Found

# Install Claude Code CLI (native installer, recommended)
curl -fsSL https://claude.ai/install.sh | bash

# Or via npm
npm install -g @anthropic-ai/claude-code

# Or set custom path in your options
# .cli_path(Some("/path/to/claude".into()))

Authentication Required

# Run claude and follow the browser login prompt
claude

# Or set an API key
export ANTHROPIC_API_KEY="sk-ant-..."

Version Mismatch

# Check CLI version (requires 2.0.0+)
claude --version

Further Reading

Support