claude-wrapper 0.5.1

A type-safe Claude Code CLI wrapper for Rust
Documentation

claude-wrapper

A type-safe Claude Code CLI wrapper for Rust

Crates.io Documentation CI License

Overview

claude-wrapper provides a type-safe builder-pattern interface for invoking the claude CLI programmatically. It follows the same design philosophy as docker-wrapper and terraform-wrapper: each CLI subcommand is a builder struct that produces typed output.

Perfect for Rust applications that need to integrate with Claude Code CLI.

Installation

cargo add claude-wrapper

Quick Start

use claude_wrapper::{Claude, QueryCommand};

#[tokio::main]
async fn main() -> claude_wrapper::Result<()> {
    let claude = Claude::builder().build()?;
    let output = QueryCommand::new("explain this error")
        .model("sonnet")
        .execute(&claude)
        .await?;
    println!("{}", output.stdout);
    Ok(())
}

Two-Layer Builder Architecture

The Claude client holds shared configuration (binary path, environment, timeout). Command builders hold per-invocation options and call execute(&claude).

Claude Client

Configure once, reuse across commands:

let claude = Claude::builder()
    .env("AWS_REGION", "us-west-2")
    .timeout_secs(300)
    .build()?;

Options:

  • binary() - Path to claude binary (auto-detected from PATH by default)
  • working_dir() - Working directory for commands
  • env() / envs() - Set environment variables
  • timeout_secs() / timeout() - Command timeout (no default)
  • arg() - Add a global argument applied to every command
  • retry() - Default [RetryPolicy] applied to every command

Command Builders

Each CLI subcommand is a separate builder. Available commands:

Query

  • QueryCommand - The workhorse; claude -p with full option coverage

MCP server management

  • McpListCommand / McpGetCommand / McpAddCommand / McpAddJsonCommand / McpRemoveCommand / McpAddFromDesktopCommand / McpResetProjectChoicesCommand / McpServeCommand

Plugins

  • PluginListCommand / PluginInstallCommand / PluginUninstallCommand / PluginEnableCommand / PluginDisableCommand / PluginUpdateCommand / PluginValidateCommand

Plugin marketplace

  • MarketplaceListCommand / MarketplaceAddCommand / MarketplaceRemoveCommand / MarketplaceUpdateCommand

Auth

  • AuthStatusCommand / AuthLoginCommand / AuthLogoutCommand / SetupTokenCommand

Misc

  • VersionCommand - Get CLI version
  • DoctorCommand - Run health check
  • AgentsCommand - List available agents
  • RawCommand - Escape hatch for unsupported subcommands

QueryCommand: The Workhorse

Full coverage of claude -p (print mode). QueryCommand exposes a builder method for every CLI flag; see docs.rs for the full list. The common ones:

Method CLI flag Purpose
model() --model Model alias or full ID
system_prompt() / append_system_prompt() --system-prompt / --append-system-prompt Override or extend the system prompt
output_format() --output-format text, json, stream-json
effort() --effort low, medium, high
max_turns() --max-turns Turn cap
max_budget_usd() --max-budget-usd Spend cap
permission_mode() --permission-mode default, acceptEdits, bypassPermissions, dontAsk, plan, auto
allowed_tools() / disallowed_tools() / tools() tool-filter flags Allow/deny/restrict tools
mcp_config() / strict_mcp_config() --mcp-config / --strict-mcp-config MCP server config
json_schema() --json-schema Structured output validation
agent() / agents_json() --agent / --agents Agent or custom agents JSON
no_session_persistence() --no-session-persistence Don't save the session to disk
dangerously_skip_permissions() --dangerously-skip-permissions Sandbox escape hatch
input_format() / include_partial_messages() --input-format / --include-partial-messages Input/streaming shape
settings() / fallback_model() / add_dir() / file() various Misc per-turn knobs

Raw session flags (prefer Session for multi-turn)

Method CLI flag Purpose
continue_session() --continue Resume most recent session
resume() --resume Resume a specific session id
session_id() --session-id Force a session id
fork_session() --fork-session Fork when resuming

For multi-turn conversations you almost always want Session instead, which threads the id across turns automatically.

Usage Examples

Simple query with model override:

let output = QueryCommand::new("explain this error: file not found")
    .model("sonnet")
    .execute(&claude)
    .await?;
println!("{}", output.stdout);

JSON output with schema validation:

let output = QueryCommand::new("generate a user struct")
    .output_format(OutputFormat::Json)
    .json_schema(r#"{"type":"object","properties":{"id":{"type":"integer"}}}"#)
    .execute(&claude)
    .await?;

With permissions and tools:

let output = QueryCommand::new("implement the feature in TASK.md")
    .model("opus")
    .permission_mode(PermissionMode::Plan)
    .allowed_tools(["Bash", "Read", "Edit", "Write"])
    .max_budget_usd(1.0)
    .execute(&claude)
    .await?;

Low-level session flags on QueryCommand (rarely needed):

// Force a specific session id
let output = QueryCommand::new("analyze the codebase")
    .session_id("my-session")
    .execute(&claude)
    .await?;

// Resume later
let output = QueryCommand::new("what did we find?")
    .resume("my-session")
    .execute(&claude)
    .await?;

Session management

For multi-turn conversations, wrap the client in an Arc and use Session. It threads the CLI session_id across turns automatically, tracks cumulative cost + history, and supports both plain-prompt and streaming turns:

use std::sync::Arc;
use claude_wrapper::{Claude, QueryCommand};
use claude_wrapper::session::Session;

let claude = Arc::new(Claude::builder().build()?);

// Fresh session
let mut session = Session::new(Arc::clone(&claude));

// Plain prompt: session_id is captured from the first turn
let first = session.send("what's 2 + 2?").await?;

// Full control: pass a configured QueryCommand. Any session-related
// flags on `cmd` are overridden with this session's current id so
// they can't conflict.
let second = session
    .execute(QueryCommand::new("explain").model("opus"))
    .await?;

// Reattach to an existing session id
let mut resumed = Session::resume(claude, "sess-abc123");
let next = resumed.send("pick up where we left off").await?;

// Cumulative state
println!("cost: ${:.4}", session.total_cost_usd());
println!("turns: {}", session.total_turns());
println!("history: {} turns", session.history().len());

Session is Send + Sync and holds an Arc<Claude>, so it can be moved between tasks or stored in long-lived actor state. Streaming turns use Session::stream / Session::stream_execute, which capture the session id from the first event that carries one — and persist it even if the stream errors partway through.

Streaming NDJSON events

For real-time output, use stream_query(). The handler is called once per parsed NDJSON line:

use claude_wrapper::{Claude, QueryCommand, OutputFormat};
use claude_wrapper::streaming::{StreamEvent, stream_query};

let claude = Claude::builder().build()?;
let cmd = QueryCommand::new("explain quicksort")
    .output_format(OutputFormat::StreamJson);

stream_query(&claude, &cmd, |event: StreamEvent| {
    if let Some(t) = event.event_type() {
        println!("[{t}]");
    }
    if event.is_result() {
        println!("result: {}", event.result_text().unwrap_or(""));
        println!("session: {}", event.session_id().unwrap_or(""));
        println!("cost: ${:?}", event.cost_usd());
    }
}).await?;

For multi-turn streaming with automatic session-id capture, use Session::stream / Session::stream_execute instead.

MCP Server Commands

List Servers

let output = McpListCommand::new()
    .execute(&claude)
    .await?;
println!("{}", output.stdout);

Add HTTP Server

McpAddCommand::new("sentry", "https://mcp.sentry.dev/mcp")
    .transport(Transport::Http)
    .execute(&claude)
    .await?;

Add Stdio Server

McpAddCommand::new("my-tool", "npx")
    .server_args(["my-mcp-server"])
    .env("API_KEY", "secret")
    .execute(&claude)
    .await?;

Add from JSON

McpAddJsonCommand::new("config.json")
    .execute(&claude)
    .await?;

Remove Server

McpRemoveCommand::new("old-server")
    .execute(&claude)
    .await?;

MCP config builder

Generate .mcp.json files programmatically:

use claude_wrapper::McpConfigBuilder;

McpConfigBuilder::new()
    .http_server("hub", "http://127.0.0.1:9090")
    .stdio_server("tool", "npx", ["my-server"])
    .write_to("/tmp/my-project/.mcp.json")?;

With the tempfile feature (on by default), build_temp() writes to a temporary file that is deleted when the returned TempMcpConfig is dropped — useful for one-shot queries.

Plugin Management

// List plugins
PluginListCommand::new().execute(&claude).await?;

// Install plugin
PluginInstallCommand::new("my-plugin")
    .execute(&claude)
    .await?;

// Enable plugin
PluginEnableCommand::new("my-plugin")
    .execute(&claude)
    .await?;

// Update plugin
PluginUpdateCommand::new("my-plugin")
    .execute(&claude)
    .await?;

Other Commands

// Check auth
AuthStatusCommand::new().execute(&claude).await?;

// Get version
VersionCommand::new().execute(&claude).await?;

// Health check
DoctorCommand::new().execute(&claude).await?;

// List agents
AgentsListCommand::new().execute(&claude).await?;

Escape hatch: RawCommand

For subcommands not yet wrapped, use RawCommand. Pass the subcommand name to new() and any flags via .arg():

let output = RawCommand::new("custom-subcommand")
    .arg("--unsupported-flag")
    .arg("value")
    .execute(&claude)
    .await?;

Error Handling

All commands return Result<T>, where errors are typed with thiserror:

use claude_wrapper::Error;

match QueryCommand::new("test").execute(&claude).await {
    Ok(output) => println!("{}", output.stdout),
    Err(Error::CommandFailed { stderr, exit_code, .. }) => {
        eprintln!("Command failed (exit {}): {}", exit_code, stderr);
    }
    Err(Error::Timeout { .. }) => eprintln!("Command timed out"),
    Err(e) => eprintln!("Other error: {}", e),
}

Features

Optional Cargo features (all enabled by default):

  • json - JSON output parsing via serde_json
  • tempfile - Temporary file support for MCP config generation

Examples

Runnable examples in examples/:

cargo run -p claude-wrapper --example oneshot          # Single query
cargo run -p claude-wrapper --example stream_query     # Streaming NDJSON events
cargo run -p claude-wrapper --example json_output      # Structured JSON response
cargo run -p claude-wrapper --example mcp_config       # MCP config generation
cargo run -p claude-wrapper --example health_check     # CLI diagnostics
cargo run -p claude-wrapper --example agent_worker     # Agent worker setup
cargo run -p claude-wrapper --example supervised_worker # Restart loop with budget tracking

Testing

Requires the claude CLI binary to be installed:

cargo test --lib --all-features
cargo test --test integration -- --ignored  # requires `claude` binary

License

MIT OR Apache-2.0