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 commandprocess_group()-- isolate each child in its own Unix process group (enabled by default)kill_grace()-- optional grace before SIGKILL on awaited shutdown pathstermination_signal()-- SIGTERM (default) or opt-in SIGINT during that graceoutput_limit()-- byte ceiling on captured stdout and stderr (no default)
Claude also exposes:
cli_version()/cli_version_sync()-- parsedCliVersioncheck_version()/check_version_sync()-- assert a minimum version
Awaited cancellation
QueryCommand::execute_cancellable and execute_json_cancellable accept any
future that resolves to (). When it resolves, the wrapper terminates the
owned process group and reaps the direct child before returning
Error::Cancelled. A configured timeout uses the same settlement path.
Dropping an ordinary execute future still triggers immediate process-group cleanup, but a destructor cannot wait for reaping. Use the cancellable methods when the caller needs terminal settlement before it records a run as stopped.
To let the CLI cancel its detached tools before escalation, opt into SIGINT and a short grace period:
use ;
use Duration;
let claude = builder
.termination_signal
.kill_grace
.build?;
Awaited shutdown drains and discards output with bounded buffers during the
full grace, then sends SIGKILL to the owned group and reaps the direct child.
Cancellation and timeout keep their original error even if the CLI exits
successfully during cleanup. This also applies to blocking timeouts, streaming,
and duplex shutdown overruns; DuplexSession::interrupt() remains a protocol
request, separate from process termination.
The defaults are unchanged: no grace means immediate SIGKILL. The soft signal
and grace apply only to owned Unix process groups. With process_group(false)
or on other platforms, shutdown kills only the direct child. Dropping a future
always uses immediate hard cleanup. Descendants that leave the owned group
require cooperative cleanup by the CLI; the wrapper cannot guarantee their
termination.
Bounded output capture
A buffered run reads the child's stdout and stderr to EOF, so peak memory is
whatever the CLI decides to print. output_limit() sets a ceiling:
let claude = builder
.output_limit
.build?;
The ceiling applies to each stream independently, so the pair holds at most
twice the configured bytes. Reaching it terminates the child's process group
(honoring kill_grace), reaps the direct child, and returns
Error::OutputLimitExceeded { stream, limit_bytes } naming which stream ran
over. It is not a truncated success: a caller can tell a complete answer from
one that exceeded what it agreed to hold. Output captured before the ceiling
tripped is logged at warn rather than returned.
There is no default ceiling. With output_limit() unset, capture behavior is
unchanged.
This bounds the buffered paths (execute, execute_json, and their
cancellable and blocking forms). Streaming runs deliver events as they arrive
and are unaffected.
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 |
extra_arg() / extra_args() |
raw CLI arguments | Unvalidated options for this query |
Use extra_arg / extra_args for provider options that need to pass through
unchanged. Each value is one argument, including values containing spaces;
the wrapper does not invoke a shell. Repeated calls append in order, after
generated options and before -- and the prompt. With prompt_via_stdin(true),
they are the final arguments and the prompt is still sent through stdin.
let cmd = new
.model
.extra_args
.extra_arg
.prompt_via_stdin;
Passthrough arguments are not validated, including conflicts with typed
options. The Claude CLI determines how duplicate or unsupported flags behave.
Frontends that depend on a specific input or output format should reject
conflicting passthrough options themselves. Use ClaudeBuilder::arg when an
argument should apply to every command instead.
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 | via Conversation (TurnResult reports the session's running total) |
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?;
Use stream_query_cancellable when a host has an explicit stop signal. It
returns only after the owned process group has been terminated and the direct
child reaped. Streaming also honors QueryCommand::prompt_via_stdin(true), so
sensitive prompts do not need to appear in process arguments.
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).
Execution diagnostics
Normal execution tracing is intended to report operation, argument counts, byte counts, timing and outcome metadata. Prompts, argument values, native identifiers, paths, configuration values, process output and rendered errors belong in caller-controlled command previews, result/error values or explicit native logs. The wrapper does not install a subscriber. Hosts choose their subscriber and correlation spans; enabling wrapper tracing must not opt them into content logs.
This contract covers execution, streaming, retries and resident sessions. Read-side filesystem-inspection modules have separate diagnostics that can include paths and parse errors; filter those targets when inspecting private local state.
Error handling
use Error;
match new.execute.await
Escape hatch: RawCommand
For subcommands not yet wrapped:
let output = new
.arg
.arg
.execute
.await?;
Compatible headless options
QueryCommand and DuplexOptions expose forward_subagent_text() for nested
stream-json messages, and allow_dangerously_skip_permissions() to make bypass
mode available without enabling it. Both flags are checked at the existing CLI
floor and maximum. Neither is enabled by default; the floor remains 2.1.220.
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)
Native permission checks
Resident permission handlers run when the CLI asks for approval. Auto-approved
actions can skip the callback; configure an explicit ask rule in settings()
for actions that must reach it. Denial and deferred denial are tested with
real Claude 2.1.220 and 2.1.303. These cases keep the resident transport open
and require no injected SDK hook configuration.
Run the focused authenticated checks with:
Set CLAUDE_INTEGRATION_BIN to test a specific installed CLI. The tests request
a benign Bash file write, always deny it and verify that no file was created.
License
MIT OR Apache-2.0