roba 0.7.1

A sharp, focused sugaring of claude -p -- pipeable, composable, safe-by-default, session-re-enterable.
Documentation

roba

Crates.io Documentation CI Downloads License

A single-prompt CLI runner on top of claude -p: one invocation, one answer. Adds composable input, pipe-clean output, re-enterable sessions, and a stable scripting ABI.

roba is sugar over the one binary -- not a platform, orchestrator, daemon, or skills framework. Point it at a quick question, a CI step, or an unattended worker.

  • Humans: prompt from files / stdin / git context, rendered markdown on a TTY, flag bundles as profiles, history, cost.
  • Agents (Claude Code): stdout is the answer, stderr is metadata, a versioned --json envelope, typed exit codes, --trace to watch a run.

Built on claude-wrapper.

$ roba "summarize the rust ownership model in 3 bullets"
   Rust's ownership model rests on three rules:

     • Each value has a single owner.
     • When the owner goes out of scope, the value is dropped.
     • Borrows are either many immutable or one mutable.

tokens 1.2k/450 . $0.0042 . 2.0s . session abc12345

Install

Source How
crates.io cargo install roba
Homebrew brew install joshrotenberg/brew/roba
Prebuilt binary latest release -- macOS (arm64 / x86_64), Linux (arm64 / x86_64), Windows; shell and PowerShell installers included

roba shells out to the claude binary, so you need claude-code installed and authenticated (or ANTHROPIC_API_KEY set) on your PATH.

The full flag, env-var, and config reference lives in the binary: roba --help. This README is the overview.

vs. claude -p directly

claude -p is the one-shot primitive: prompt in, response to stdout, exit. roba keeps that model and adds:

Adds How
Composable input -f file, piped stdin (the prompt, or context when a prompt is given), -e editor, --prepend / --append, --attach globs, --git-diff / --git-log / --git-status, --var template vars
Pipe-clean output stdout is the answer; all metadata (footer, spinner, tool lines, warnings) goes to stderr -- piping to jq sees a clean stream
TTY rendering markdown, spinner, color while it runs; --plain / NO_COLOR turns it off
Session re-entry -c continue, -c ID resume, --fork, --pick chooser, --session-id mints a caller-chosen id, --session NAME resumes a named handle ([session] in roba.toml); roba history / roba last browse past runs
Read-only inspection roba show <ID> prints a stored run's result (--metrics, --wait); roba worktree list; roba history --worktree NAME finds a runner's session
A stable scripting ABI typed exit codes, versioned --json envelope, clean stream split -- see For agents & scripts

For interactive, multi-turn work, use claude itself; for multiple providers, llm. roba is Claude-only: the Claude-Code-native integration (sessions, permissions, history) is the point.

Quick examples

# Just ask
roba "what's the difference between Arc and Rc?"

# Compose: preamble + question + appendix
roba --prepend system.md "review this design" --append context.md

# Pull in files by glob
roba --attach 'src/**/*.rs' "is the error handling consistent?"

# Read-only review against the working-tree diff
roba --readonly --git-diff "is this safe to merge?"

# Continue the most recent session here (pass the prompt with -p, since
# -c takes an optional session id)
roba -c -p "now show me how to test the unsafe variant"

# scripted multi-turn: mint an id once, reuse it on later turns
# (bare `claude -p --continue` no-ops in print mode; this is the fix)
uuid=$(uuidgen)
roba --session-id "$uuid" "start a refactor plan"
roba -c="$uuid" "now do step 1"

# Pipe-friendly: answer only, stdin in
roba "what's 2+2" -q            # prints "4"
echo "summarize this" | roba    # stdin works, no flag needed
cat err.log | roba "what's wrong here?"   # piped data becomes context

[!NOTE] -c (continue) and -w (worktree) take an optional value, so a space-separated word right after them is read as that value: roba -c "follow up" treats follow up as the session id. Pass the prompt explicitly with -p to disambiguate: roba -c -p "follow up".

Safe by default

roba starts read-only: claude may use Read, Glob, and Grep and nothing else. You opt into more, explicitly:

roba "explain this"                      # read-only (default)
roba --writable "rename foo to bar"      # add Edit + Write
roba --allow-tool "Bash(git:*)" "..."    # add one specific pattern
roba --deny-tool WebFetch "..."          # block one (deny wins)
roba --add-dir ../shared "..."           # widen file-tool scope to another dir
roba --full-auto "..."                   # bypass every check (sandbox only)
roba --show-permissions --profile review # preview the resolved set, then exit

--add-dir (repeatable) passes through to claude's --add-dir: claude's file tools are cwd-scoped by default; each --add-dir adds one directory.

The read-only start does not regress. --permission-mode additionally sets claude's own approval mode (plan, acceptEdits, ...), orthogonal to the allow-list. Precedence across all layers: CLI flag > ROBA_* env > profile > top-level config keys > built-in default, and deny always wins over allow.

To give claude extra tools from an MCP server for one run, point it at a server config file:

roba --mcp-config mcp.json "..."                  # add those servers' tools
roba --mcp-config mcp.json --strict-mcp-config "..." # use ONLY those servers

--mcp-config (repeatable) passes through to claude's --mcp-config: roba forwards the path, claude reads it. Not a roba MCP server -- it wires per-run MCP servers into the claude -p call.

Configuration: profiles & aliases

A roba.toml lets you stop retyping flags and define your own verbs. Files are discovered by walking up from the cwd (plus ~/.config/roba.toml); closer-to-cwd wins per key.

  • Profiles are named bundles of flag defaults: --profile review applies [profile.review]. A default profile auto-applies.
  • Aliases are new verbs: roba review 42 expands an [alias.review] prompt template (${1} / ${pr} / $(...) shell substitution) plus default flags and dispatches like a normal call. Your domain knowledge lives in your aliases, not the binary.
  • Draft one with claude: roba alias draft "..." / roba profile draft "..." generate a validated [alias.NAME] / [profile.NAME] block from a description on stdout (pipe it to >> roba.toml); --write appends it for you.

The fully-commented roba-config.sample.toml documents every key with worked examples; roba profile init drops it in your project. Inspect with roba profile {list,show,active} and roba alias {list,show}.

For ready-to-copy setups, examples/ carries vetted bundles (each parse-tested in CI): roba-rust-dispatch.toml -- a worker-dispatch config proven on a real backlog run, with fix and design aliases -- and roba-reviewer.toml, an enforced read-only, schema-validated reviewer in a few lines of TOML.

For agents & scripts

The contract: stdout is the answer, stderr is everything else, and --json wraps every output in a versioned envelope.

success: { "version": 1, "result": { ... }, "refusal": bool }    (stdout)
failure: { "version": 1, "error": { kind, message, exit_code, chain } }    (stderr)

The read-only management commands (cost, history, doctor, worktree list) emit the same { "version": 1, "result": ... } shape (minus the ask-only refusal); roba show reconstructs the full ask envelope, refusal included. One parser handles every --json output. Pin version and you've pinned the shape.

Reading the envelope:

Fact Detail
The answer .result.result -- not .result (that's the whole object)
Metrics nest under .result: .result.duration_ms, .result.num_turns, .result.total_cost_usd (serde rename of cost_usd); the top-level paths return null
Top level only version, refusal, and result
Refusal still exits 0 (the call succeeded) -- detect via the refusal field, not the exit code
Exit codes 0 ok, 1 generic (incl. --max-turns / --max-budget-usd cap hits), 2 auth, 3 budget, 4 timeout; the error kind maps the same way
see_also omitted when empty -- don't assume the key exists
out=$(roba --json "..."); echo "$out" | jq -r '.result.result'

Worker flags:

Flag Does
--json-schema PATH schema-validated model output; roba reads the file and inlines it (claude's flag wants inline JSON); surfaces under .result.*, no extra output flag needed
--max-turns N, --max-budget-usd USD rails for unattended loops; hitting a cap errors the run (exit 1)
--no-retry surface transient failures immediately; the caller owns retry
--trace PATH the spawned session's events as JSONL -- watch a run in flight
--fallback-model MODEL retry on a second model when the primary is overloaded
--no-session-persistence run without writing a resumable session record
--full-auto unsupervised editing worker; add --worktree for parallel same-repo workers (for orchestrator-owned branches use git worktree add + -C <dir> instead -- roba's --worktree is claude-managed, on a branch you won't PR from)
roba doctor --json boundary checks as { checks: [{ name, status, message }], overall }; exits 0 when no check fails, 1 otherwise

Near the end of a turn the trace carries claude's post_turn_summary system event (status_category + status_detail) -- a usable done / what-happened signal, but it is claude's event passed through, not part of roba's versioned ABI.

Work that must outlive the caller

A run owns nothing once it returns -- any sub-work still in flight dies with it, and nothing resumes on its own. So there are two shapes for work, and only two: synchronous units (each unit finishes inside the turn and the caller loops), or detached-with-handle when the hand-off itself is the point.

The detached form, primary:

id=$(roba --detach --profile worker -C <dir> -f task.md --trace /tmp/t.jsonl)
roba show "$id" --wait --timeout 600

The minted session handle is the only thing on stdout, so id=$(...) captures it. roba verifies the claude binary resolves before detaching, so it refuses rather than print a handle for a dead-on-arrival child. A piped stdin that carries data is refused rather than silently lost (the detached child's stdin is /dev/null; this data check is unix-only for now). Nobody is watching the run, so pair it with the rails (--max-turns, --max-budget-usd).

The manual fallback (older versions, or no --detach):

nohup roba --session-id "$(uuidgen)" -C <dir> -f task.md >/dev/null 2>&1 &
roba show <id> --wait

Never bare fire-and-forget: an orphaned branch and an empty draft PR are the signature of that failure.

The orchestrator side of the same rule: if you orchestrate from inside roba -p, you get exactly ONE turn. When the model stops calling tools and writes its final response, the process exits -- there is no re-invocation and no cross-turn background-completion notification (that is a persistent-harness feature, not a -p one). So either block in-foreground (roba show <id> --wait) or hand the session handle back to your caller explicitly; never background a task and stop expecting to auto-resume. roba injects this as a system-prompt advisory by default (disable with --no-agent-notice, replace with --agent-notice).

Address a session by a STABLE handle, not -c. roba -c continues the most recent session in the project, which silently drifts when an orchestrator spawns its own roba sub-invocations (a detached worker, a roba show, a roba profile init) in the same directory -- the newest one out-ranks the session you meant to resume. Pin it instead: pass the short session id (roba resolves a unique prefix) or a named [session] handle from your roba.toml.

[!NOTE] As of 2026-06-15 Anthropic meters programmatic usage (claude -p / Agent SDK) separately from interactive Claude. Every roba call is programmatic by construction, so all roba usage -- and the figures roba cost reports -- draws from that programmatic allotment, not your interactive limit.

Bring your own skills and agents

roba is a pure mechanical wrapper -- no bundled skill or agent library. Drop skills into ~/.claude/skills/ and agents into ~/.claude/agents/; Claude Code auto-discovers them. joshrotenberg/agent-tools is one curated set if you want a starting point.

Status

Published on crates.io. The CLI surface (flag names, exit codes, config schema, --json envelope) is intended to be stable across 0.x; the library API (roba::*, for integration testing) may shift between minor versions.

License

MIT OR Apache-2.0