# claude-wrapper
A type-safe Claude Code CLI wrapper for Rust
[](https://crates.io/crates/claude-wrapper)
[](https://docs.rs/claude-wrapper)
[](https://github.com/joshrotenberg/claude-wrapper/actions/workflows/ci.yml)
[](LICENSE-MIT)
## 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`](https://crates.io/crates/docker-wrapper) and [`terraform-wrapper`](https://crates.io/crates/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
```bash
cargo add claude-wrapper
```
## Quick Start
```rust
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:
```rust
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`](https://docs.rs/claude-wrapper/latest/claude_wrapper/struct.QueryCommand.html) for the full list. The common ones:
| `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)
| `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`](#session-management) instead, which threads the id across turns automatically.
### Usage Examples
Simple query with model override:
```rust
let output = QueryCommand::new("explain this error: file not found")
.model("sonnet")
.execute(&claude)
.await?;
println!("{}", output.stdout);
```
JSON output with schema validation:
```rust
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:
```rust
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):
```rust
// 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?;
```
<a id="session-management"></a>
## 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:
```rust
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:
```rust
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
```rust
let output = McpListCommand::new()
.execute(&claude)
.await?;
println!("{}", output.stdout);
```
### Add HTTP Server
```rust
McpAddCommand::new("sentry", "https://mcp.sentry.dev/mcp")
.transport(Transport::Http)
.execute(&claude)
.await?;
```
### Add Stdio Server
```rust
McpAddCommand::new("my-tool", "npx")
.server_args(["my-mcp-server"])
.env("API_KEY", "secret")
.execute(&claude)
.await?;
```
### Add from JSON
```rust
McpAddJsonCommand::new("config.json")
.execute(&claude)
.await?;
```
### Remove Server
```rust
McpRemoveCommand::new("old-server")
.execute(&claude)
.await?;
```
## MCP config builder
Generate `.mcp.json` files programmatically:
```rust
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
```rust
// 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
```rust
// 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()`:
```rust
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`:
```rust
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/`:
```bash
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:
```bash
cargo test --lib --all-features
cargo test --test integration -- --ignored # requires `claude` binary
```
## License
MIT OR Apache-2.0