roba
A sharp, focused sugaring of claude -p --
pipeable, composable, safe-by-default, session-re-enterable.
roba (Venetian): "stuff, things." roba returns the stuff you asked about --
roba is the bag, the answer the contents.
arrows to advance -- down dives into a chapter
man roba
claude-wrapper; Claude-only by design$ 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
roba --help | diff claude-p -
claude -p directly| Adds | How |
|---|---|
| Composable input | -f file, piped stdin, -e editor, --prepend/--append, --attach globs, --git-diff/--git-log/--git-status, --var |
| Pipe-clean output | stdout = answer; footer, spinner, tool lines, warnings all on stderr |
| TTY rendering | markdown, spinner, color while it runs -- gone when the answer lands |
| Session re-entry | -c continue, --fork, --pick, --session-id mints a caller-chosen id |
| Read-only inspection | roba show (--metrics, --wait), worktree list, history --worktree |
| A stable scripting ABI | typed exit codes, versioned --json envelope, clean stream split |
For interactive multi-turn work: use claude itself. That's the point.
roba --show-permissions
Starts read-only: Read, Glob, Grep and nothing else. Everything beyond is an explicit opt-in:
roba "explain this" # read-only (default)
roba --writable "rename foo to bar" # add Edit + Write
roba --allow-tool "Bash(git:*)" "..." # one specific pattern
roba --deny-tool WebFetch "..." # block one (deny wins)
roba --add-dir ../shared "..." # widen file scope
roba --full-auto "..." # bypass all (sandbox only)
the two axes, with receipts
roba --show-permissions --profile worker
--readonly / --writable / --allow-tool / --deny-tool; deny always wins--permission-mode (plan, acceptEdits, ...)--full-auto is the exception: bypasses both, on purpose, loudly documented$ roba --show-permissions --profile worker
all tools allowed (--full-auto from profile.worker)
Resolved set + provenance ([default] / [env] / [profile.NAME]), then exit. Zero cost, no call -- the receipt for every permission question.
roba history
# dip back into a thread
roba -c -p "now add a test for that"
# scripted multi-turn: mint an id once, reuse it
# (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"
inspection + named handles
roba show 2fca1a5d --metrics --wait
roba show <id> -- a stored run's result, reconstructed from its JSONL: duration_ms null, turns/cost derived, honestly labeled--wait [--timeout] -- poll until the last assistant turn's stop_reason goes terminal (best-effort heuristic over the log, never a hang)roba worktree list -- the repo's git worktrees (a superset of claude's); list-onlyroba history --worktree NAME -- find a dispatched runner's session to resumeThe hard line: roba never writes .claude/ -- sessions, worktrees, credentials are claude's domain. Inspect and report, only.
roba --session meta -p "weekly groom"
[session] # in roba.toml -- machine-local
meta = "8b273dbb-50d1-4cfa-9e2f-1c30a9b7e441"
ci-bot = "f12e9f36-2a01-47bb-bd58-77c2b41d09aa"
--session NAME / ROBA_SESSION=NAME resolve the handle through the pool--session-id), then bind it -- names exist before first runroba --json | jq
success: { "version": 1, "result": { ... }, "refusal": bool } (stdout)
failure: { "version": 1, "error": { kind, message, exit_code, chain } } (stderr)
| Exit | Meaning |
|---|---|
| 0 | ok -- refusals included; detect via refusal, not the code |
| 1 | generic failure, incl. --max-turns / --max-budget-usd cap hits |
| 2 / 3 / 4 | auth / budget / timeout |
observability
tail -f trace.jsonl
--trace PATH -- the spawned session's events as JSONL, live, on every path (even under --json)--json output carries version: 1 -- ask, cost, history, doctor, show, worktree list. One parser for everything.result.result; metrics nest under .resultroba show <id> --wait -- poll a stored run until it completes, then renderroba --json "..." | jq -r '.result.result'
jq survival
jq -r '.result.result'
| You want | The path |
|---|---|
| the answer | .result.result -- not .result (that's the whole object) |
| metrics | .result.duration_ms, .result.num_turns, .result.total_cost_usd (a serde rename of cost_usd); top-level paths return null |
| "did it refuse?" | .refusal, top-level -- a refusal still exits 0 |
| what broke | .error.kind in the stderr envelope; see_also is omitted when empty |
# capture with printf, not echo -- zsh's echo interprets the
# JSON's backslash escapes and corrupts it
out=$(roba --json "..."); printf '%s' "$out" | jq -r '.result.result'
UNSUNG HERO 01
Named bundles of flag defaults. Nothing magical --
and that's exactly why they're powerful.
six slides down: resolution, the pool, the surface, the theory
roba profile show worker
A profile is a TOML alias for flags you'd otherwise type every time. CLI always wins.
[profile.review]
readonly = true
git_diff = true
roba --profile review "is this safe to merge?"
# identical to: roba --readonly --git-diff "is this safe to merge?"
A profile named `default` auto-applies. ROBA_PROFILE=name selects by env. --no-default-profile kills auto-apply.
roba --show-permissions --profile review
Every knob resolves the same way, highest layer wins:
1. CLI flag -- you typed it, it wins
2. ROBA_* env var -- lists comma-separated; truthy bools only enable
3. active [profile.NAME] -- the overlay
4. top-level roba.toml keys -- the floor you set
5. roba's built-in defaults -- read-only, fresh session
6. claude's defaults
Deterministic and inspectable: --show-permissions prints the resolved set with provenance ([default] / [env] / [profile.NAME]) and exits. No call, no guessing.
roba profile path
~/.config/roba.toml + every roba.toml walking up from cwd to the git root[vars] merge per-key[session] binds NAME = "uuid" handles for --session NAME -- config, not stateroba owns zero runtime state. The pool is a pure function of the files; parse errors are loud (unknown keys are hard errors, listing every valid key).
grep -c '=' roba-config.sample.toml
| Dimension | Keys |
|---|---|
| Posture | readonly, writable, full_auto, allow_tool, deny_tool, permission_mode |
| Context | prepend, append, attach, git_diff, git_log, git_status, vars |
| Identity | model, effort, fallback_model, agent, system_prompt, append_system_prompt |
| Session | continue, session_id, worktree, no_session_persistence |
| Output | json, json_schema, quiet, plain, stream, echo, show_thinking, trace, no_dollars, rates_file |
| Rails | max_turns, max_budget_usd, no_retry, bare, no_agent_check |
| Tools | mcp_config, strict_mcp_config, add_dir |
The annotated sample (roba profile init) documents every key and is parse-tested in CI -- config docs that cannot go stale.
: the theory
A "review skill" asks the model to stay read-only and look at the diff.
Model compliance is a coin flip -- that lesson is paid for.
[profile.review] doesn't ask:
json_schema output is validated, not requested: the theory, part two
A profile pins both ends of a run -- the setup (posture, context, model, rails) and the finish line (the output contract). Only the reasoning in between belongs to the model.
That's most of what people want from a lightweight agent -- with zero prose to drift and zero interpretation risk.
Where it tops out: procedure. Sequence and judgment are irreducibly prose -- that's the skill layer's job, and it lives outside the binary.
# an enforced-read-only,
# diff-fed, schema-validated
# reviewer. nine lines. no
# skill. nothing to ignore.
[profile.review]
readonly = true
git_diff = true
append_system_prompt = """
Review for correctness.
Cite file:line. No nits."""
json_schema = "findings.json"
max_turns = 10
cat ~/.config/roba.toml
# the proven unattended-worker shape, wearing its rails
[profile.worker]
full_auto = true
max_turns = 80 # a true spiral trips them,
max_budget_usd = 10.0 # a heavy 20-edit run clears them
# long-horizon: fable primary, opus on overload
[profile.fable]
model = "claude-fable-5"
fallback_model = "claude-opus-4-8"
# cheap fast one-shots
[profile.quick]
model = "claude-haiku-4-5"
Plus review / explain / commit-msg / fix-build on the human side. The dispatch line is now: roba --profile worker -C repo -f task.md --trace t.jsonl
UNSUNG HERO 02
New verbs. roba review 42 expands a prompt template + flags
and dispatches like a normal call.
five slides down: the schema, the template language, a worked verb
roba alias show review
[alias.review]
description = "Review a PR by number"
agent = "reviewer" # pin a claude-code subagent
args = ["pr"] # positional 1 -> ${pr}
flags = ["--readonly"] # merged BEFORE your CLI flags
template = """
Review PR #${pr} in this repo.
Diff:
$(gh pr diff ${pr})
"""
description, agent, args, flags, template -- that's the whole schema.
man roba-template
| Form | Means |
|---|---|
${1} ${2} ... | positional args after the verb (1-based) |
${@} | all positional args, space-joined |
${name} | named arg, resolved via the args schema |
$$ | a literal $ -- dollar amounts and shell vars survive |
$(command) | shell substitution: runs in your shell, stdout interpolated |
$(...) is NOT sandboxed -- it's your shell, before claude ever runs. Orthogonal to claude's permissions. Don't template a command you wouldn't type.
roba review 42
roba review 42
|
| 1. lookup: [alias.review] in the pool (built-ins win; shadowing warns)
| 2. args: 42 -> ${pr}
| 3. shell: $(gh pr diff 42) runs NOW, in your shell
| 4. flags: --readonly merged in (your CLI flags still win)
| 5. agent: reviewer pinned
v
roba --readonly --agent reviewer "Review PR #42 ... [the actual diff]"
The diff is IN the prompt before claude starts -- mechanically, not because a skill asked the model to go fetch it.
roba r "look at the auth module"
[alias.r]
description = "Quick read-only review preset"
agent = "reviewer"
flags = ["--readonly"] # no template: args become the prompt
flags merge before CLI flags -- your CLI always winsroba alias {list, show, path}Your domain knowledge lives in your aliases, not the binary.
roba alias list
NAME DESCRIPTION
cm Conventional-commit message from the staged diff
issue Summarize an issue and propose an approach
review Review a PR by number
wtf Diagnose piped failure output
roba review 244 # gh pr diff, in the prompt
roba issue 242 # gh issue view, summarized
git add -p && roba cm # staged diff -> commit message
cargo build 2>&1 | roba wtf # piped failure, diagnosed
Adding review/issue surfaced a real bug the same day: ${arg} wasn't substituted inside $(...) -- fixed within the hour (#247). The heroes earn their keep by being used.
: the stack
| Layer | Carries | Character |
|---|---|---|
| Profile | params: posture, context, model, rails, output contract | fully deterministic -- enforced by machinery |
| Alias | params + a prompt template + live shell context | deterministic setup, parameterized prose |
| Skill | procedure: sequence, judgment, lifecycle | prose a model interprets -- lives outside roba |
Each step up adds expressiveness and sheds determinism.
Push everything parametric down the stack -- reserve prose for what only prose can do.
roba --profile worker
The proven dispatch loop: orchestrator owns branch + PR, roba does the code work. ~16 consecutive dispatched PRs shipped v0.5.0 itself -- zero spirals.
roba --profile worker -C ~/code/repo -f /tmp/task.md --trace /tmp/t.jsonl
--json-schema for validated structured output--mcp-config for per-run tools (a pass-through, not a daemon)cat err.log | roba "what's wrong here?"exit codes as recovery signals
echo $?
| Signal | The orchestrator's move |
|---|---|
2 auth | halt the whole fleet and re-auth -- nothing else will succeed |
4 timeout / transient 1 | re-fire the same task (--no-retry hands the retry decision to you) |
1 at a rail | under-scoped: raise --max-turns / --max-budget-usd, or split the task |
0 + refusal: true | not success -- branch on the field, never the code alone |
And the real verdict is downstream anyway: the worker is graded on its artifact -- a commit, a PR, green CI -- not its exit code.
roba doctor
| Source | How |
|---|---|
| crates.io | cargo install roba |
| Homebrew | brew install joshrotenberg/brew/roba |
| Binaries | macOS arm64/x86_64, Linux arm64/x86_64, Windows -- shell + PowerShell installers on the latest release |
roba --help + parse-tested config sample. The documentation is the skill.github.com/joshrotenberg/roba -- MIT OR Apache-2.0