roba 0.2.0

Single-prompt CLI runner built on claude-wrapper
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
# roba profiles

A profile is a named bundle of `roba` flags you'd otherwise type every
time. Nothing magical -- the profile only fills in fields you didn't
pass on the command line. CLI flags always win.

```bash
roba --profile review "what changed and is it safe to merge?"
```

## Where profiles live

`roba` builds a merged pool from a chain of `roba.toml` files. Later
sources override earlier ones; when the same profile name appears in
more than one file, fields merge per-key (closer-to-cwd file wins on
scalars, lists concat, vars merge per-key).

1. **User-level:** `$XDG_CONFIG_HOME/roba.toml` or
   `~/.config/roba.toml`. Your global baseline.
2. **Project chain:** every `roba.toml` walking up from the current
   directory; stops at the git root if there is one, else the
   filesystem root. Closer-to-cwd files override farther ones on
   the same key.

Top-level keys in a `roba.toml` are project-wide defaults: every
roba call in the dir inherits them. `[profile.NAME]` tables sit
above those defaults and activate via `--profile NAME` (or
`ROBA_PROFILE`, or by being named `default`).

Missing files are fine. `roba profile path` prints what's currently
in the chain so you can see which file would supply which name.

## Env-var overrides

Every config knob is also settable via an env var matching the CLI
long-form, uppercased with `-` -> `_` and prefixed `ROBA_`. Sits
between CLI flags (top priority) and the file pool, so you can
override one knob for a single shell session without editing a file:

```bash
ROBA_WRITABLE=1 roba "rename the foo variable to bar"
ROBA_MODEL=opus roba "review this design"
ROBA_GIT_LOG=10 roba "summarize recent work"
ROBA_ALLOW_TOOL="Bash(git status),Bash(git diff)" roba "..."
ROBA_VAR_TICKET=ABC-123 roba --profile commit-msg "..."
```

Value rules:

- **String** (e.g. `ROBA_MODEL`): any non-empty value.
- **Bool** (e.g. `ROBA_WRITABLE`): truthy `1`/`true`/`yes`/`on`
  (case-insensitive) enables. Other values are ignored -- the env
  layer can only enable a bool, never disable a file-set one.
- **Number** (e.g. `ROBA_GIT_LOG=5`): parsed; invalid values
  silently ignored.
- **List** (e.g. `ROBA_ALLOW_TOOL`, `ROBA_PREPEND`): comma-separated,
  whitespace trimmed, empty entries dropped.
- **Vars**: one env var per key, `ROBA_VAR_<KEY>=<value>`.

Like files, env-var overrides only fill fields the user didn't set
on the CLI. `roba --writable=false ...` -- well, there's no such
flag, since `--writable` is presence-flagged -- but
`--allow-tool Edit` on the CLI fully replaces any `ROBA_ALLOW_TOOL`
list, same as it overrides a profile's `allow_tool` list.

The one setting that *does* have an explicit kill switch is
`continue`: pass `--fresh` to force a new session even when a profile
or env var sets `continue = true` (or pins a specific session id).
Pair it with `ROBA_CONTINUE=1` enabled in a project default to opt out
for one call without re-typing config.

## Auto-apply and explicit invocation

When `roba` runs the default ask path:

1. `roba --profile NAME ...` -> apply `NAME`. Error if it doesn't exist.
2. `roba --no-default-profile ...` -> skip auto-apply for this call.
3. `ROBA_PROFILE=NAME roba ...` (env) -> apply `NAME` as the default.
4. If a `default` profile exists in the pool -> apply it silently.
5. Otherwise no profile.

Explicit `--profile` always wins. `--no-default-profile` bypasses
both steps 3 and 4 in one go.

To see what would auto-apply without making a call:

```bash
roba profile active
```

## Schema

Every field is optional -- specify only what you want a profile to
override.

| Field | Type | Maps to | Notes |
|---|---|---|---|
| `prepend` | `[path]` | `--prepend PATH` (repeatable) | `~/` is expanded |
| `append` | `[path]` | `--append PATH` (repeatable) | `~/` is expanded |
| `attach` | `[glob]` | `--attach GLOB` (repeatable) | |
| `git_diff` | `bool` | `--git-diff` | |
| `git_log` | `int` | `--git-log N` | |
| `git_status` | `bool` | `--git-status` | |
| `readonly` | `bool` | `--readonly` | Explicit form of the default; suppresses lower-layer `writable` / `full_auto` |
| `writable` | `bool` | `--writable` | Adds Edit + Write to the allow list |
| `full_auto` | `bool` | `--full-auto` | Bypass all permission checks |
| `allow_tool` | `[string]` | `--allow-tool TOOL` (repeatable) | Adds to the allow list |
| `deny_tool` | `[string]` | `--deny-tool TOOL` (repeatable) | Deny patterns; useful with `full_auto` to keep some teeth |
| `continue` | `bool` or `string` | `-c` / `-c=ID` | `true` continues the most recent session in the directory; a string resumes that specific session id (e.g. `continue = "7c3f9a21"`); `false` stays fresh. CLI `-c` / `-c=ID` overrides it |
| `vars` | `{ key = "value" }` | `--var KEY=VALUE` (repeatable) | CLI keys override profile keys |
| `model` | `string` | `--model MODEL` | Alias (`sonnet`/`opus`/`haiku`) or full id (`claude-sonnet-4-6`) |
| `effort` | `string` | `--effort LEVEL` | Effort level: `low`, `medium`, `high`, `xhigh`, `max`. Controls cost/quality tradeoff. Profile payoff: `[profile.thorough]` with `effort = "max"`, `[profile.quick]` with `effort = "low"` |
| `agent` | `string` | `--agent NAME` | Pin a claude-code subagent by name; must exist in `.claude/agents/NAME.md` (or be auto-discovered per claude's lookup) |
| `stream` | `bool` | `--stream` | Stream tokens as they arrive |
| `show_thinking` | `bool` | `--show-thinking` | Render extended-thinking blocks live on stderr. Only takes effect with `--stream`; ignored otherwise |
| `echo` | `bool` | `--echo` | Print resolved prompt before the response |
| `plain` | `bool` | `--plain` | No rendering, color, or spinner |
| `quiet` | `bool` | `-q` / `--quiet` | Answer only, no metadata |
| `json` | `bool` | `--json` | Structured result as JSON on stdout |
| `editor_history` | `int` | `--editor-history N` | With `-e`, pre-fill the editor with the last N assistant responses, separated by a scissors line. Default 1; 0 disables |
| `worktree` | `bool` or `string` | `-w` / `--worktree[=NAME]` | `true` runs every session in a fresh git worktree (claude generates the name); a string pins the worktree directory/branch (e.g. `worktree = "feature-x"`) |
| `bare` | `bool` | `--bare` | Skip hooks, LSP, plugin sync, CLAUDE.md auto-discovery, auto-memory, and keychain reads. Agent-tier flag. |
| `no_retry` | `bool` | `--no-retry` | Disable wrapper-level auto-retry on transient failures; the failure surfaces immediately with its normal exit code |
| `trace` | `string` | `--trace PATH` | Write the spawned session's streaming events to PATH as JSONL (a stable observability handle); `~/` is expanded. Forces the streaming pipeline internally even without `--stream` |
| `rates_file` | `string` | `--rates-file PATH` | Override the bundled per-model rates table used for the footer dollar figure (same schema as `roba cost --rates-file`); `~/` is expanded. Also honored via `ROBA_RATES_FILE` |
| `no_dollars` | `bool` | `--no-dollars` | Omit the dollar figure from the per-call footer (tokens only); useful when the bundled rates are stale |
| `system_prompt` | `string` | `--system-prompt TEXT` | Replace the default system prompt entirely for this call |
| `append_system_prompt` | `string` | `--append-system-prompt TEXT` | Append to the default system prompt; useful for per-profile role framing (e.g. `"You are a senior code reviewer"`) |

Unknown keys are rejected at parse time -- a typo errors fast instead
of being silently ignored.

## CLI overrides profile

Two rules:

1. If you pass a scalar flag on the CLI, it wins (e.g. `--git-log 7`
   beats a profile's `git_log = 3`).
2. If you pass a list/repeatable flag on the CLI, the CLI list
   *replaces* the profile list entirely -- they don't concatenate.

For `vars`, the same idea but per-key: CLI `--var NAME=foo` overrides
the profile's `NAME` and the rest of the profile's vars still apply.

## Precedence (full layer list)

When the same knob is set in more than one place, the highest
layer wins:

1. **CLI flag** (e.g. `--writable`, `--allow-tool Edit`)
2. **Env var** (e.g. `ROBA_WRITABLE=1`, `ROBA_ALLOW_TOOL=Edit,Write`)
3. **Active `[profile.NAME]` overlay** (selected explicitly,
   auto-applied via `ROBA_PROFILE`, or named `default`)
4. **Top-level keys** in any `roba.toml` (closer-to-cwd files
   win for scalars; lists and vars merge as described above)
5. **roba's built-in defaults** (read-only permissions, no
   composition, no streaming)

The env layer runs first, then the profile layer; both check
"did the CLI / a prior layer already set this?" before filling
in a value. So a CLI flag is never overridden by env or profile,
and an env var is never overridden by profile.

### Permissions precedence

For the `readonly` / `writable` / `full_auto` knobs, the highest
layer that sets one wins, and a higher-priority flag **suppresses**
the more-permissive ones from lower layers. `full_auto` also
short-circuits `writable` at apply time. So:

- profile `writable = true` + no CLI/env override -> writable
  (Edit, Write added)
- profile `writable = true` + CLI `--full-auto` -> full-auto
  (bypasses everything)
- profile `full_auto = true` + CLI `--writable` -> writable
  (CLI `--writable` suppresses the lower-layer `full_auto`)
- profile `writable = true` + CLI `--readonly` -> read-only
  (CLI `--readonly` suppresses the lower-layer `writable`)

`--readonly` is the explicit name for the default, and it is an
**active suppressor**: passing `--readonly` cancels a
`writable = true` or `full_auto = true` coming from a lower layer,
so the call stays read-only. It also pairs cleanly with permissive
list additions (`--readonly --allow-tool "Bash(git status)"`).
Symmetrically, `--writable` suppresses a lower-layer
`full_auto = true`. See #52.

For `allow_tool` and `deny_tool`, lists **accumulate across
layers**:

- Across `roba.toml` files, closer-to-cwd entries concat on top
  of farther-from-cwd entries.
- The active profile's list concats on top of the merged
  top-level list.
- The CLI (`--allow-tool` / `--deny-tool`, repeatable) and env
  (`ROBA_ALLOW_TOOL` / `ROBA_DENY_TOOL`, comma-separated)
  **replace** the resolved list when set -- they don't
  concatenate with the lower layer.

When the same tool ends up in both `allow_tool` and `deny_tool`
(across any combination of layers), **deny wins**. roba passes
both resolved lists through to claude unchanged; claude is the
final arbiter.

## Worked examples

Drop these into `~/.config/roba.toml` (or a project-local
`roba.toml`). They're starting points, not opinions -- adapt to
your habits.

### `default` -- per-project auto-apply

Drop this into your project's `roba.toml` and `roba "..."` in
that tree picks it up without `--profile`:

```toml
# /path/to/my-project/roba.toml
[profile.default]
readonly = true
continue = true
prepend = ["CLAUDE.md"]
```

What happens:

- `cd /path/to/my-project && roba "what does this do"` ->
  read-only tools, continues the most recent session, prepends
  your project's CLAUDE.md to every prompt.
- `cd ~ && roba "..."` -> no default applies (no `roba.toml`
  walking up).
- `roba --no-default-profile "..."` -> bypass even inside the project.

### `default` with project-aware permissions

Discussion + read-only git introspection. Lets claude run
`git status` / `git diff` to ground its answers without opening up
branch operations or edits:

```toml
[profile.default]
readonly = true
continue = true
allow_tool = [
    "Bash(git status)",
    "Bash(git diff)",
    "Bash(git diff --stat)",
    "Bash(git log)",
    "Bash(git log --oneline)",
]
```

`readonly` seeds the allow list with Read/Glob/Grep; `allow_tool`
adds these specific Bash patterns. Anything else (Edit, Write,
`Bash(git checkout)`, etc.) still gets blocked.

### `review` -- code review on current changes

```toml
[profile.review]
readonly = true
git_diff = true
```

Usage:

```bash
roba --profile review "is the auth change safe to merge?"
```

What it does: locks claude to read-only tools (no edits, no shell)
and embeds your working-tree diff into the prompt. Add a prepend
file with your own review style if you want stronger opinions:

```toml
[profile.review]
readonly = true
git_diff = true
prepend = ["~/.config/roba/prompts/review-style.md"]
```

### `explain` -- read-only walkthrough

```toml
[profile.explain]
readonly = true
```

Usage:

```bash
roba --profile explain --attach 'src/foo.rs' "what does this module do, and what assumptions does it make?"
```

Pairs well with `--attach`. The profile keeps claude from poking at
anything you didn't ask about.

### `commit-msg` -- generate a commit message from staged work

```toml
[profile.commit-msg]
readonly = true
git_diff = true

[profile.commit-msg.vars]
STYLE = "imperative, concise, no marketing"
```

Usage:

```bash
roba --profile commit-msg "write a commit message in the {{STYLE}} style"
```

The `STYLE` placeholder is substituted from the profile's vars. You
can override per-invocation:

```bash
roba --profile commit-msg --var STYLE="bullet points" "write a commit message in the {{STYLE}} style"
```

### `summarize` -- distill long content

```toml
[profile.summarize]
readonly = true

[profile.summarize.vars]
LENGTH = "one paragraph"
```

Usage with stdin:

```bash
cat long-doc.md | roba --profile summarize "summarize this in {{LENGTH}}, plain prose"
```

### `fix-build` -- diagnose a failed build from piped output

```toml
[profile.fix-build]
readonly = true
git_status = true
```

Usage:

```bash
cargo build 2>&1 | roba --profile fix-build "what's broken and how do I fix it?"
```

The `git_status` line gives claude context on which files you've been
editing -- often the actual culprit isn't obvious from the error
alone.

### `ticket` -- thread a project label through a template

```toml
[profile.ticket]
git_log = 3

[profile.ticket.vars]
PROJECT = "MYPROJ"
```

Usage with a template file:

```bash
# ~/.config/roba/prompts/standup.md
# Write today's standup in the {{PROJECT}} format. Recent commits:

roba --profile ticket -f ~/.config/roba/prompts/standup.md
```

### `gh-context` -- read-only PR / issue context via the gh CLI

A profile that lets claude pull GitHub context (PR / issue / diff /
list) through the `gh` CLI without opening the door to mutations:

```toml
[profile.gh-context]
description = "Read-only gh access for PR / issue context"
readonly = true
allow_tool = [
    "Bash(gh pr view:*)",
    "Bash(gh pr diff:*)",
    "Bash(gh pr list:*)",
    "Bash(gh issue view:*)",
    "Bash(gh issue list:*)",
    "Bash(gh repo view:*)",
]
```

Usage:

```bash
roba --profile gh-context "summarize the open PRs in this repo"
roba --profile gh-context "what does PR #42 do?"
```

`readonly = true` seeds the allow list with Read / Glob / Grep; the
`Bash(gh ...:*)` patterns add the read-only gh commands on top. The
`:*` suffix is Claude Code's prefix matcher -- it allows the command
plus any arguments (a PR number, `--json` flags), so `Bash(gh pr
view:*)` covers `gh pr view 42 --json title`. Claude can pull context
but can't `gh pr merge`, `gh pr close`, or run any verb you didn't
list.

For mutations, layer a more permissive profile or add the specific
patterns explicitly (`--allow-tool "Bash(gh pr comment:*)"`). Keep the
deny side in mind too: `deny_tool` wins over `allow_tool`, so a broad
allow plus a targeted deny is a valid shape if you'd rather subtract.

## Aliases

Aliases are `git`-style shortcuts defined in the same `roba.toml`
files as profiles (same walk-up + merge discovery). Where a profile is
a *named bundle of flag defaults* you opt into with `--profile`, an
alias is a *new verb*: `roba NAME [args]` expands a prompt template
plus default flags and dispatches like a normal call.

The full schema, lookup order, variable + shell substitution, and
caveats live in [`aliases.md`](aliases.md).

## Tips

- **Keep prompts in files, not vars.** Profiles are best at flag
  defaults; for long prompt templates, prefer `prepend = ["..."]`
  pointing at a markdown file. Easier to edit, version-control, and
  share.
- **Layer a profile and ad-hoc flags freely.** `--profile review
  --full-auto` is fine if you want the review preset *and* tool
  bypass for that one call.
- **Inspect what a profile sets** by combining with `--echo`:

  ```bash
  echo "" | roba --profile review --echo -q
  ```

  prints the assembled prompt to stderr without making a real call.
- **Share profiles with your team** by checking
  `roba.toml` into a dotfiles repo or by dropping a copy in your
  project root and pointing `XDG_CONFIG_HOME` at it for that shell.

## Future

- Inline prompt text in profile schema (`prepend_inline = ["..."]`)
  so a profile can carry a prompt without a separate file