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 - Long-lived
DuplexSessionfor hosts (IDE backends, daemons, agent servers, chat UIs): oneclaudesubprocess held open across turns, mid-turn interrupts, mid-turn permission decisions, broadcast event subscribers - Transient
Sessionfor short-lived processes (CLIs, build scripts, batch jobs): subprocess-per-turn with--resumecontinuity, cumulative cost + history, optionalBudgetTrackerhard-stops, streaming Conversationfor host-side history/cost/budget bookkeeping over aDuplexSession- NDJSON streaming events (
stream_query/stream_query_sync) - Typed tool-permission patterns (
ToolPattern) - Read-side introspection of Claude Code's on-disk state without spawning the CLI: session history, agents, skills, custom commands, settings, background jobs, worktrees
- MCP server management: list, get, add, add-json, remove, add-from-desktop, login, logout, serve, reset-project-choices
- Plugin and marketplace subcommands
- Other subcommands:
doctor,install,update,project purge,auto-mode, andultrareview(cloud-hosted multi-agent code review) - 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.13", = 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 commandclear_env()-- clear inherited child environment before applyingenv()/envs()arg()-- 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
Child environment policy
Children inherit the parent process environment by default for compatibility. Hosts that need an explicitly constructed environment can opt into clearing it:
let claude = builder
.clear_env
.env
.env
.env
.build?;
clear_env() is call-order independent: explicit entries survive whether
env() / envs() appear before or after it. Every buffered, stdin, timeout,
retry, streaming, sync, and duplex child uses the same policy.
Clearing the environment controls only what the direct Claude CLI child
inherits. It is not operating-system or process isolation. A same-UID child
may still read accessible files or inspect other processes where the OS
permits it. Callers are responsible for choosing the minimal PATH, locale,
config-home, credential, proxy, Git, and SSH entries appropriate for their
deployment.
Command builders
| Category | Builders |
|---|---|
| Query | QueryCommand |
| MCP | McpListCommand, McpGetCommand, McpAddCommand, McpAddJsonCommand, McpRemoveCommand, McpAddFromDesktopCommand, McpLoginCommand, McpLogoutCommand, McpServeCommand, McpResetProjectChoicesCommand |
| Plugins | PluginListCommand, PluginInstallCommand, PluginUninstallCommand, PluginEnableCommand, PluginDisableCommand, PluginUpdateCommand, PluginValidateCommand, PluginDetailsCommand, PluginPruneCommand, PluginTagCommand |
| Marketplace | MarketplaceListCommand, MarketplaceAddCommand, MarketplaceRemoveCommand, MarketplaceUpdateCommand |
| Auth | AuthStatusCommand, AuthLoginCommand, AuthLogoutCommand, SetupTokenCommand |
| Auto-mode | AutoModeConfigCommand, AutoModeDefaultsCommand, AutoModeCritiqueCommand |
| Lifecycle | InstallCommand, UpdateCommand, ProjectPurgeCommand |
| Review | UltrareviewCommand |
| 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.
Multi-turn conversations
Two shapes for multi-turn work, each suited to a different process
model. Pick DuplexSession if you can keep a claude subprocess
open for the lifetime of the conversation (IDE backends, daemons,
agent servers, chat UIs). Pick Session if your process is
short-lived and conversation continuity is the main thing you need
(CLIs, build scripts, batch jobs, lambdas).
DuplexSession |
Session |
|
|---|---|---|
| Process model | one child held open across turns | new subprocess per turn, --resume continuity |
| Mid-turn interrupt | yes (interrupt()) |
no (only child.kill() via SIGKILL) |
| Mid-turn permission prompts | yes (PermissionHandler) |
no |
| Broadcast event subscribers | yes (subscribe()) |
no (per-turn streaming via stream_query) |
| Built-in cost / history tracking | no (TurnResult is per-turn) | yes (total_cost_usd, history, BudgetTracker) |
| Right for | long-running hosts | short-lived processes |
DuplexSession (recommended for long-running hosts)
Holds one claude subprocess open in stream-json duplex mode for
the duration of a conversation. User messages go in over stdin,
NDJSON events come back over stdout. Subscribe to the event stream
for token-by-token UI updates, answer permission prompts via an
async callback, and cancel a turn cleanly with interrupt().
use Claude;
use ;
let claude = builder.build?;
let session = spawn.await?;
let mut events = session.subscribe;
let turn = session.send.await?;
while let Ok = events.try_recv
println!;
session.close.await?;
See the duplex module docs
for interrupt(), respond_to_permission(), and the full surface.
Session (for short-lived processes)
Spawns a transient subprocess per turn and threads session_id
across them via --resume. Tracks history, cumulative cost, and
integrates with BudgetTracker for hard-stop ceilings. The right
fit when each turn can stand on its own and the host process
isn't long-lived.
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.).
On-disk introspection
Read Claude Code's on-disk state under ~/.claude directly, without
spawning the CLI. Each module exposes a root/loader with list / get
accessors and returns an empty result when the directory is absent.
| Module | Type | Reads |
|---|---|---|
history |
HistoryRoot |
sessions and transcripts (projects/<slug>/<id>.jsonl) |
artifacts |
AgentsRoot |
agent definitions (agents/<name>.md) |
skills |
SkillsRoot |
skill definitions (skills/<name>/SKILL.md) |
commands |
CommandsRoot |
custom slash commands |
settings |
SettingsLoader |
the four settings layers, merged by precedence |
jobs |
JobsRoot |
background-agent / supervisor state |
worktrees |
WorktreeRoot |
git worktrees for --worktree sessions |
use HistoryRoot;
let history = home?;
for project in history.list_projects?
The history, jobs, and settings modules require the json
feature (on by default). See the inspect_state example for a tour.
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.13", = false, = ["json", "sync"] }
Examples
Most examples use the async API (--features async, on by default);
inspect_state is read-only and needs just --features json.
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