# roba
[](https://crates.io/crates/roba)
[](https://docs.rs/roba)
[](https://github.com/joshrotenberg/roba/actions/workflows/ci.yml)
[](https://crates.io/crates/roba)
[](#license)
A single-prompt CLI runner built on top of `claude -p`. One invocation,
one answer, done -- but with composable input, pipe-clean output,
sessions you can re-enter without living in them, and a stable scripting
ABI for agents and scripts.
- **For humans** -- what `claude -p` could be: build the prompt from
files / stdin / git context, get rendered markdown on a TTY, save
flag bundles as profiles, browse history, track cost.
- **For agents** in [Claude Code](https://github.com/anthropics/claude-code)
-- a stable contract: stdout is the answer, stderr is metadata, a
versioned `--json` envelope, typed exit codes, and `--trace` for
observing a run in flight.
Built on [`claude-wrapper`](https://crates.io/crates/claude-wrapper).
```console
$ 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 · 2.0s · session abc12345
```
## Install
```bash
cargo install roba
# or: brew install joshrotenberg/brew/roba
```
`roba` shells out to the `claude` binary, so you need
[claude-code](https://github.com/anthropics/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 conceptual tour.
## Why not just `claude -p`?
`claude -p` is the one-shot primitive: one prompt in, the response to
stdout, exit. It's perfect for a quick "just answer this string" with no
piping, no file context, no continuity. roba doesn't replace it -- it
gives that same one-invocation-one-answer model a richer surface, for
when you want any of:
- **Composable input.** Build the prompt from more than a string: a
file (`-f`), piped stdin, an editor buffer (`-e`), `--prepend` /
`--append` files, glob-embedded files (`--attach`), git context
(`--git-diff` / `--git-log` / `--git-status`), and `{{KEY}}` template
vars (`--var`).
- **Pipe-clean output.** stdout is the answer and *only* the answer;
every bit of metadata (cost footer, spinner, tool-call lines, refusal
warnings) goes to stderr. `roba "..." | jq` always sees a clean pipe.
- **Rich on a TTY.** Rendered markdown, a spinner, dim metadata, colored
markers -- a transient UI that exists while roba works and evaporates
when the answer lands. `--plain` (or `NO_COLOR`) turns it all off.
- **Session re-entry without living in a session.** `-c` continues the
most recent session in the directory, `-c ID` resumes a specific one,
`--fork` branches it, `--pick` is a fuzzy chooser; `roba history` /
`roba last` browse past runs. You dip back into a thread without
opening the TUI.
- **A real ABI.** Typed exit codes, a versioned `--json` envelope, and a
clean stream split -- so a script or agent can pin a contract instead
of scraping prose. (See [For agents & scripts](#for-agents--scripts).)
Same one-shot model, a citizen of the pipe. For *interactive,
multi-turn* work, reach for `claude` itself; for *multiple providers*,
a tool like [`llm`](https://llm.datasette.com/). roba is Claude-only by
design -- the Claude-Code-native integration (sessions, permissions,
history) is the point.
## Quick examples
```bash
# 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"
# Pipe-friendly: answer only, stdin in
roba "what's 2+2" -q # prints "4"
> [!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:
```bash
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 --full-auto "..." # bypass every check (sandbox only)
roba --show-permissions --profile review # preview the resolved set, then exit
```
This came from a real "oops" -- a streaming run quietly let claude
create a git branch when the user just wanted a chat. The default
doesn't 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 > built-in default**, and deny always wins over allow.
## 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 (with `${1}` / `${pr}` / `$(...)`
shell substitution) plus default flags, and dispatches like a normal
call. This is how you keep roba's built-in surface generic -- your
domain knowledge lives in *your* aliases, not the binary.
The fully-commented [`roba-config.sample.toml`](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 agents & scripts
When something other than a human is calling, roba is a much cleaner ABI
than `claude -p`:
- **stdout = the answer, stderr = everything else.** `roba "..." | jq`
never sees decoration.
- **Versioned `--json` envelope.** Success:
`{ "version": 1, "result": { ... }, "refusal": bool }`. Failure:
`{ "version": 1, "error": { kind, message, exit_code, chain } }`.
Pin `version` and you've pinned the shape.
- **Typed exit codes:** `0` ok, `1` generic, `2` auth, `3` budget,
`4` timeout.
- **`--no-retry`** surfaces transient failures immediately (the caller
decides whether to retry), and **`--trace PATH`** writes the spawned
session's events as JSONL so you can observe a run in flight.
- **`--dispatch`** is the unattended preset (`--full-auto --worktree
--fresh`) for firing a worker that edits files in its own sandbox.
For an agent that *invokes* roba, [`skills/use-roba/SKILL.md`](skills/use-roba/SKILL.md)
documents this contract in agent-readable form -- copy it to
`~/.claude/skills/use-roba/SKILL.md`.
## 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](https://github.com/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