Using the terminal · 13
Headless mode
riffer -p "prompt" runs one prompt with no terminal and exits with a code that says how it ended. It loads everything the terminal does — settings, credentials, extensions, skills, AGENTS.md — so CI jobs, cron jobs and other programs drive riffer-rig just as you would.
The prompt
riffer -p "what does this repo do"
git diff | riffer -p "review this diff" # stdin is added to the prompt as context
echo "explain this diff" | riffer -p # stdin alone is the prompt
Slash commands are not run: the text goes to the model as typed.
With a prompt argument, riffer waits up to 3 seconds for stdin to send data or close. If it does neither, it warns on stderr and runs the argument alone; once data arrives, it reads stdin to the end. A program that spawns riffer with a stdin pipe it never closes (Node’s child_process.spawn, Python’s subprocess.PIPE) pays those 3 seconds on every run, and a shell loop that reads its own stdin loses the rest of its input to the first run. Redirect stdin from /dev/null in both cases:
while read -r p; do riffer -p --no-save "$p" </dev/null; done < prompts.txt
Output
Only the assistant’s reply goes to stdout, streamed as it arrives. Everything else goes to stderr: errors, warnings, notices from extensions, and session <id> when the session starts — the id riffer -p -r <id> "follow-up" resumes — and any --verbose traces. Reasoning is not printed.
Flags
Every terminal flag works here, plus --json, --verbose and --job-idle-timeout (Command line).
-c and -r <id> continue a session across runs: riffer -p -c "now run the tests" carries on the most recent session in this directory (Resuming).
Headless never asks anything. A missing model, credential or SDK gem prints what to set or install on stderr and exits 2, as does a settings or auth.json file that doesn’t parse. A project rig.rb you haven’t trusted in the terminal is skipped.
Background jobs
When a turn ends with background jobs still running:
| When | Headless |
|---|---|
| the turn finished | prints waiting for background jobs on stderr and waits; each exit gives the model a further turn to react, printed like the first, and the run exits once no job is left |
a wait passes the --job-idle-timeout with no job exiting |
kills the rest and exits 3, so a dev server or file watcher can’t hold a run open forever |
| the turn failed, was cancelled or didn’t complete | kills its jobs and exits at once |
Exit codes
| Exit | Meaning |
|---|---|
0 |
the turn finished, and so did every background-job follow-up |
1 |
runtime error: provider or network failure, an exception in a tool or extension |
2 |
usage or configuration: unknown flag, no prompt, a bare model name, no credentials, a missing SDK gem, a file that doesn’t parse, no session to resume |
3 |
the turn ended without completing — the step limit, the context window, an output length limit, a content filter, malformed output — or background jobs passed the idle timeout; the reason is on stderr |
130 |
interrupted (SIGINT): the turn is cancelled, background jobs are killed, and the session is saved |
With follow-up turns, the code is the last turn’s.
The NDJSON stream (--json)
--json replaces the reply text on stdout with one JSON object per line, one for every event of the run. Each record is flat: the event’s fields plus a type.
{"type":"session_start","id":"0198…","reason":"new"}
{"role":"assistant","content":"Reading the failing test first.","type":"text_done"}
{"role":"assistant","item_id":"call_1","call_id":"call_1","name":"read","arguments":"{\"path\":\"x\"}","type":"tool_call_done"}
{"type":"before_tool_call","call_id":"call_1","tool":"read","args":{"path":"x"},"at":81234.513}
{"type":"after_tool_call","call_id":"call_1","tool":"read","args":{"path":"x"},"result":{"content":"1: …","error":null,"error_type":null},"at":81234.531,"duration":0.018}
{"stop_reason":"completed","usage":{"input_tokens":1423,"output_tokens":89},"type":"turn_end"}
The records are the events the run streams — riffer’s stream events and riffer-rig’s own — so a consumer that only reads the last turn_end still knows how the turn ended and what it cost. Headless adds one record of its own: a fatal error ends the stream with {"type": "error", "message": "…"}.
Notices that text mode prints on stderr arrive as notify records instead. The stdin warning, waiting for background jobs, a turn’s turn ended: … reason and --verbose traces stay on stderr.