claude-wrapper
A type-safe Rust wrapper around the Claude Code CLI.
Overview
claude-wrapper gives you a builder-pattern interface for the claude
CLI. Each subcommand is a typed builder that produces a typed output.
The design follows the same shape as
docker-wrapper and
terraform-wrapper.
Features:
- Full coverage of
claude -pviaQueryCommand - Async (tokio) and blocking (
std::thread+wait-timeout) APIs behind feature flags - Multi-turn
Sessionwith auto-resume, streaming, cumulative cost, and optionalBudgetTrackerhard-stops - NDJSON streaming events (
stream_query/stream_query_sync) - Typed tool-permission patterns (
ToolPattern) - MCP server management: list, get, add, add-json, remove, add-from-desktop, serve, reset-project-choices
- Plugin and marketplace subcommands
- Auth:
status,login,logout,setup-token McpConfigBuilderfor programmatic.mcp.jsongeneration (with optional tempfile backing)- Retry policy with fixed or exponential backoff
- Opt-in
DangerousClientfor bypass-permissions mode (env-var gated) RawCommandescape hatch for subcommands not yet wrapped
Installation
Default features: ["async", "json", "tempfile"]. To drop tokio
entirely for a sync-only build:
= { = "0.6", = false, = ["json", "sync"] }
Quick start (async)
use ;
async
Quick start (sync)
Enable the sync feature and use ClaudeCommandSyncExt:
use ;
The sync API uses std::process::Command plus wait-timeout for the
SIGKILL-on-deadline path, with dedicated reader threads for stdout and
stderr so the child never blocks on pipe-buffer backpressure. It matches
the async version's behaviour feature-for-feature (timeouts, retries,
large-output drain, non-Send handlers for streaming).
Two-layer builder
The Claude client holds shared configuration (binary path,
environment, timeout, default retry policy). Command builders hold
per-invocation options and call execute(&claude) (or execute_sync).
let claude = builder
.env
.timeout_secs
.retry
.build?;
Claude options:
binary()-- path to theclaudebinary (auto-detected from PATH by default)working_dir()/with_working_dir()-- working directory for commandsenv()/envs()-- environment variables applied to every commandarg()-- global arg applied before every subcommandtimeout_secs()/timeout()-- command timeout (no default)retry()-- defaultRetryPolicyapplied to every command
Claude also exposes:
cli_version()/cli_version_sync()-- parsedCliVersioncheck_version()/check_version_sync()-- assert a minimum version
Command builders
| Category | Builders |
|---|---|
| Query | QueryCommand |
| MCP | McpListCommand, McpGetCommand, McpAddCommand, McpAddJsonCommand, McpRemoveCommand, McpAddFromDesktopCommand, McpServeCommand, McpResetProjectChoicesCommand |
| Plugins | PluginListCommand, PluginInstallCommand, PluginUninstallCommand, PluginEnableCommand, PluginDisableCommand, PluginUpdateCommand, PluginValidateCommand |
| Marketplace | MarketplaceListCommand, MarketplaceAddCommand, MarketplaceRemoveCommand, MarketplaceUpdateCommand |
| Auth | AuthStatusCommand, AuthLoginCommand, AuthLogoutCommand, SetupTokenCommand |
| Misc | VersionCommand, DoctorCommand, AgentsCommand, RawCommand |
Every builder implements ClaudeCommand (args(), async execute).
With the sync feature, ClaudeCommandSyncExt provides a blanket
execute_sync on every command that returns CommandOutput.
QueryCommand additionally has inherent execute_sync /
execute_json_sync that honour the retry policy;
AuthStatusCommand has execute_json_sync.
QueryCommand
Full coverage of claude -p. The common options:
| 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 |
Per-call spend cap (CLI-side) |
permission_mode() |
--permission-mode |
default, acceptEdits, dontAsk, plan, auto |
allowed_tool() / allowed_tools() / disallowed_tool() / disallowed_tools() |
tool filter flags | Allow/deny tool patterns |
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 |
input_format() / include_partial_messages() |
--input-format / --include-partial-messages |
Input/streaming shape |
settings() / fallback_model() / add_dir() / file() |
various | Misc per-turn knobs |
retry() |
(client-side) | Per-command retry override |
Raw session flags (prefer Session for multi-turn):
| Method | CLI flag |
|---|---|
continue_session() |
--continue |
resume() |
--resume |
session_id() |
--session-id |
fork_session() |
--fork-session |
Bypass-permissions mode is only available via
DangerousClient; see below.
JSON output
let result = new
.output_format
.json_schema
.execute_json
.await?;
println!;
println!;
Tool permissions
Use ToolPattern for first-class, validated patterns:
use ;
let cmd = new
.allowed_tool
.allowed_tool
.allowed_tool
.allowed_tool
.disallowed_tool;
Bare strings still work via From<&str>:
let cmd = new.allowed_tools;
ToolPattern::parse(s) validates shape (balanced parens, no comma,
non-empty name) and returns a typed PatternError on malformed input.
Session management
For multi-turn conversations, wrap the client in an Arc and use
Session. It threads session_id across turns, tracks cumulative cost
- history, and supports streaming.
use Arc;
use ;
use Session;
let claude = new;
let mut session = new;
let first = session.send.await?;
let second = session
.execute
.await?;
println!;
println!;
Resume an existing session:
let mut resumed = resume;
let next = resumed.send.await?;
Session is Send + Sync, holds Arc<Claude>, and can move between
tasks or sit in long-lived actor state. Streaming turns use
Session::stream / Session::stream_execute and capture the session
id from the first event that carries one -- persisted even if the
stream errors partway through.
Session is currently async-only. Sync callers compose equivalent
state using execute_sync / execute_json_sync and BudgetTracker.
Budget tracking
Attach a BudgetTracker to a session (or share one across several
sessions) to enforce USD ceilings:
use BudgetTracker;
let budget = builder
.max_usd
.warn_at_usd
.on_warning
.on_exceeded
.build;
let mut session = new.with_budget;
session.send.await?;
println!;
println!;
Callbacks fire exactly once at their thresholds. When max_usd is
set, Session::execute / Session::stream_execute short-circuit with
Error::BudgetExceeded before dispatching any turn that would run
with the ceiling already hit -- a hard stop, not just a callback.
Clone a tracker across several sessions to share the running total.
BudgetTracker is internally Arc<Mutex<_>> so clones are cheap and
the total is coherent.
Streaming NDJSON
use ;
use ;
let claude = builder.build?;
let cmd = new
.output_format;
stream_query.await?;
Sync: stream_query_sync. The handler runs on the caller's thread, so
it can capture non-Send state (Rc<RefCell<_>>, etc.).
MCP servers
// List
new.execute.await?;
// HTTP
new
.transport
.execute
.await?;
// Stdio
new
.server_args
.env
.execute
.await?;
// From JSON
new.execute.await?;
// Remove
new.execute.await?;
Programmatic .mcp.json
use McpConfigBuilder;
new
.http_server
.stdio_server
.write_to?;
With the tempfile feature (default), build_temp() returns a
TempMcpConfig that writes to a temp file and deletes it on drop --
useful for one-shot queries.
Dangerous: bypass mode
--permission-mode bypassPermissions is isolated behind
DangerousClient, which requires an env-var acknowledgement at
process start:
use ;
use DangerousClient;
// Set CLAUDE_WRAPPER_ALLOW_DANGEROUS=1 at process start
let dangerous = new?;
let output = dangerous
.query_bypass
.await?;
Construction fails with Error::DangerousNotAllowed if the env var is
absent. There is no equivalent on plain Claude -- the type-level
isolation is deliberate.
Retry policy
use Duration;
use RetryPolicy;
let policy = new
.max_attempts
.initial_backoff
.exponential
.retry_on_timeout
.retry_on_exit_codes;
let claude = builder.retry.build?;
Retry wraps every QueryCommand::execute and execute_sync (other
commands don't retry). A per-command override is available via
QueryCommand::retry(policy).
Error handling
use Error;
match new.execute.await
Escape hatch: RawCommand
For subcommands not yet wrapped:
let output = new
.arg
.arg
.execute
.await?;
Cargo features
| Feature | Default | Purpose |
|---|---|---|
async |
yes | tokio-backed async API. Disabling drops tokio from the runtime dep tree. |
json |
yes | JSON output parsing (execute_json, StreamEvent, Session, stream_query). |
tempfile |
yes | TempMcpConfig for one-shot MCP config files. |
sync |
no | Blocking API: *_sync methods on exec, retry, each command builder, and Claude. Pulls in wait-timeout. |
Sync-only build with no tokio:
= { = "0.6", = false, = ["json", "sync"] }
Examples
All examples use the async API and require --features async (on by
default).
Testing
# Unit + doc tests
# Integration tests against the bundled fake-claude.sh (no real CLI needed)
# Sync-only build + tests (no tokio)
# Integration tests against a real `claude` binary (requires auth)
License
MIT OR Apache-2.0