claude-cli-sdk 0.1.0

Rust SDK for programmatic interaction with the Claude Code CLI
Documentation
claude-cli-sdk-0.1.0 has been yanked.

claude-cli-sdk

Crates.io docs.rs CI License: MIT OR Apache-2.0

Rust SDK for programmatic interaction with the Claude Code CLI.

Disclaimer: This is an unofficial, community-developed SDK and is not affiliated with, endorsed by, or sponsored by Anthropic, PBC. "Claude" and "Claude Code" are trademarks of Anthropic. This crate interacts with the Claude Code CLI but does not contain any Anthropic proprietary code.

Provides strongly-typed, async-first access to Claude Code sessions via the CLI's NDJSON stdio protocol. Supports one-shot queries, streaming responses, multi-turn sessions, permission callbacks, lifecycle hooks, and MCP server configuration.

Platform support: macOS, Linux, and Windows.

Features

  • query() — one-shot: send a prompt, collect all response messages.
  • query_stream() — streaming: yield messages as they arrive.
  • Client — stateful multi-turn sessions with send() and receive_messages().
  • Permission callbacksCanUseToolCallback for programmatic tool approval.
  • Lifecycle hooksHookMatcher on PreToolUse / PostToolUse / Stop events.
  • MCP server config — attach external MCP servers to a session.
  • Message callback — observe or filter messages before they reach your code.
  • testing featureMockTransport, ScenarioBuilder, and message builders for unit tests without a live CLI.

Quick Start

Add to Cargo.toml:

[dependencies]
claude-cli-sdk = "0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

One-shot query

use claude_cli_sdk::{query, ClientConfig};

#[tokio::main]
async fn main() -> claude_cli_sdk::Result<()> {
    let config = ClientConfig::builder()
        .prompt("List the files in /tmp")
        .build();

    let messages = query(config).await?;

    for msg in &messages {
        if let Some(text) = msg.assistant_text() {
            println!("{text}");
        }
    }

    Ok(())
}

Streaming query

use claude_cli_sdk::{query_stream, ClientConfig};
use tokio_stream::StreamExt;

#[tokio::main]
async fn main() -> claude_cli_sdk::Result<()> {
    let config = ClientConfig::builder()
        .prompt("Explain ownership in Rust")
        .model("claude-opus-4-6")
        .build();

    let mut stream = query_stream(config).await?;
    tokio::pin!(stream);

    while let Some(msg) = stream.next().await {
        let msg = msg?;
        if let Some(text) = msg.assistant_text() {
            print!("{text}");
        }
    }

    Ok(())
}

Multi-turn session

use claude_cli_sdk::{Client, ClientConfig};
use tokio_stream::StreamExt;

#[tokio::main]
async fn main() -> claude_cli_sdk::Result<()> {
    let config = ClientConfig::builder()
        .prompt("Start a Rust project")
        .build();

    let mut client = Client::new(config)?;
    let session_info = client.connect().await?;
    println!("Session: {}", session_info.session_id);

    // First turn (prompt sent via config).
    let mut stream = client.receive_messages()?;
    tokio::pin!(stream);
    while let Some(msg) = stream.next().await {
        // process...
        drop(msg);
        break;
    }
    drop(stream);

    // Subsequent turns.
    let stream2 = client.send("Now add a test suite")?;
    tokio::pin!(stream2);
    while let Some(msg) = stream2.next().await {
        if let Some(text) = msg?.assistant_text() {
            print!("{text}");
        }
    }

    client.close().await
}

Permission callback

use claude_cli_sdk::{ClientConfig, PermissionMode, PermissionDecision, PermissionContext};
use std::sync::Arc;

let config = ClientConfig::builder()
    .prompt("Edit main.rs")
    .permission_mode(PermissionMode::Default)
    .can_use_tool(Arc::new(|tool_name: &str, _input: &serde_json::Value, _ctx: PermissionContext| {
        let tool_name = tool_name.to_owned();
        Box::pin(async move {
            // Approve only file reads and writes.
            if tool_name.starts_with("Read") || tool_name.starts_with("Write") {
                PermissionDecision::allow()
            } else {
                PermissionDecision::deny("Only file tools are allowed")
            }
        })
    }))
    .build();

ClientConfig Options

Field Type Description
prompt String Required. Initial prompt text.
model Option<String> Model name, e.g. "claude-opus-4-6".
cwd Option<PathBuf> Working directory for the CLI process.
max_turns Option<u32> Maximum agentic turns before stopping.
max_budget_usd Option<f64> Cost cap for the session.
permission_mode PermissionMode Default, AcceptEdits, Plan, or BypassPermissions.
can_use_tool Option<CanUseToolCallback> Per-tool permission callback.
system_prompt Option<SystemPrompt> Text or preset system prompt.
allowed_tools Vec<String> Allowlist of tool names.
disallowed_tools Vec<String> Blocklist of tool names.
mcp_servers McpServers External MCP server definitions.
hooks Vec<HookMatcher> Lifecycle hook registrations.
resume Option<String> Resume an existing session by ID.
verbose bool Enable verbose CLI output.
connect_timeout Option<Duration> Deadline for spawn + init (default: 30s).
close_timeout Option<Duration> Deadline for graceful shutdown (default: 10s).
read_timeout Option<Duration> Per-message recv deadline (default: None).
default_hook_timeout Duration Hook callback fallback timeout (default: 30s).
version_check_timeout Option<Duration> --version check deadline (default: 5s).

Timeout configuration

use std::time::Duration;
use claude_cli_sdk::ClientConfig;

let config = ClientConfig::builder()
    .prompt("Analyze this project")
    .connect_timeout(Some(Duration::from_secs(60)))  // Wait up to 60s for init
    .read_timeout(Some(Duration::from_secs(300)))     // Detect hung processes after 5min
    .close_timeout(Some(Duration::from_secs(5)))      // Kill after 5s on close
    .default_hook_timeout(Duration::from_secs(10))    // Hook callbacks get 10s
    .build();

Set any Option<Duration> timeout to None to wait indefinitely.

Lifecycle hooks

Register callbacks for tool execution events:

use claude_cli_sdk::{ClientConfig, HookMatcher, HookEvent, HookOutput, HookDecision};
use std::sync::Arc;

let config = ClientConfig::builder()
    .prompt("Refactor auth module")
    .hooks(vec![
        HookMatcher::new(HookEvent::PreToolUse, Arc::new(|input, _session, _ctx| {
            Box::pin(async move {
                eprintln!("Tool: {:?}", input.tool_name);
                HookOutput::allow()
            })
        })).for_tool("Bash"),
    ])
    .build();

MCP server configuration

Attach external MCP servers to a session:

use claude_cli_sdk::{ClientConfig, McpServerConfig, McpServers};

let mut servers = McpServers::new();
servers.insert(
    "filesystem".into(),
    McpServerConfig::new("npx")
        .with_args(["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]),
);

let config = ClientConfig::builder()
    .prompt("List files using the filesystem MCP server")
    .mcp_servers(servers)
    .build();

Testing with MockTransport

Enable the testing feature for unit tests without a live CLI:

[dev-dependencies]
claude-cli-sdk = { version = "0.1", features = ["testing"] }
use std::sync::Arc;
use claude_cli_sdk::Client;
use claude_cli_sdk::config::ClientConfig;
use claude_cli_sdk::testing::{ScenarioBuilder, assistant_text};

let transport = ScenarioBuilder::new("test-session")
    .exchange(vec![assistant_text("Hello!")])
    .build();
let transport = Arc::new(transport);

let mut client = Client::with_transport(
    ClientConfig::builder().prompt("test").build(),
    transport,
).unwrap();

Feature Flags

Feature Description
testing MockTransport, ScenarioBuilder, and message builder helpers for unit tests
efficiency Reserved for future throughput optimizations
integration Integration test helpers (requires a live CLI)

License

Licensed under either of Apache License, Version 2.0 or MIT License at your option.