Exit codes
Every roost command ends by exiting with one of a fixed set of codes. An agent should branch
on the code, not on stderr text — the code is the contract; the message is for humans.
The full table
Section titled “The full table”| Code | Name | Meaning | What the caller should do |
|---|---|---|---|
| 0 | OK | The command completed. | Read the output. |
| 1 | GENERIC | An error that doesn’t fit a more specific code (e.g. an unexpected upstream HTTP status). | Log it, don’t blind-retry. |
| 2 | USAGE | Bad flags or arguments — an invalid date, --nights 0, --check-out before --check-in, an unparseable value. | Fix the invocation; the message names the bad flag and shows a valid example. |
| 3 | EMPTY | The query was valid but the backend itself returned zero properties for the location and stay — raised before any --class/--brand/other filter runs, so filters are never the cause. Only search and rates can exit this way; dates reports a no-availability date as a row with available: false instead. | Widen the dates or try a broader/different location. (If you passed filters and got back an empty properties/rates array at exit 0 instead of this code, that’s the filters narrowing a non-empty result to nothing — relax those instead.) |
| 4 | AUTH | The serpapi backend needs a key that isn’t set. | roost auth login --token-stdin, or set ROOST_SERPAPI_KEY. |
| 5 | NOT_FOUND | A specific lookup failed — the location couldn’t be resolved to a place, or a rates property id wasn’t found. | List available options (e.g. roost search) to find a valid identifier. |
| 6 | PERM | A request was refused before it was sent, because the path falls outside roost’s robots-respecting allowlist. | This is a roost bug, not a caller mistake — report it. |
| 7 | RATE | roost’s own local politeness throttle blocked the request — not an upstream rate limit. | Wait the given number of seconds and retry, or pass --wait --max-wait <n> to let roost sleep for you. |
| 8 | RETRY | A transient failure — network error, upstream 5xx. | Retry shortly; it’s reasonable to retry this one. |
| 10 | CONFIG | Local environment problem — no usable HTTP client installed, the pinned browser fingerprint is unavailable, or doctor found a failing check. | Fix the environment (reinstall, upgrade the HTTP client); reroll won’t help. |
| 12 | MUTATION_BLOCKED | Defined for contract uniformity; unreachable in roost. | N/A — see below. |
| 13 | INPUT_REQUIRED | A value was needed but neither supplied as a flag nor promptable (--no-input is set). | Pass the value explicitly as a flag or argument. |
| 20 | BLOCKED | Google served a challenge, consent interstitial, or soft block. | Stop. Wait for the circuit breaker to expire, or switch --backend serpapi. Do not retry immediately. |
| 21 | SCHEMA_DRIFT | The upstream response didn’t parse the way roost expects. | Upgrade roost (roost version --check); if already current, file an issue. Retrying will not help. |
| 130 | CANCELLED | The user or environment interrupted the command (Ctrl-C). | No action; this wasn’t a tool failure. |
The structured error shape
Section titled “The structured error shape”Every error, in --json mode, is a single object on stderr:
{ "error": "upstream served a challenge or soft block: HTTP 429", "code": "BLOCKED", "remediation": "wait 30s for the circuit breaker to expire, or use --backend serpapi; do NOT retry immediately"}code is a stable machine string (BLOCKED, SCHEMA_DRIFT, RATE_LIMITED, AUTH_REQUIRED,
NOT_FOUND, USAGE, and so on) — more specific than the numeric exit code, since several
distinct codes can share the same exit number (both SCHEMA_DRIFT from a bad upstream response
and PLACE_NOT_FOUND can’t happen on the same call, but RATE_LIMITED and a plain USAGE
error both exit non-zero and need different handling). Parse code, not the prose in error.
remediation is written for an agent to act on directly — it already names the flag or command
to run next. Without --json, the same three fields print as labeled lines on stderr instead:
error: upstream served a challenge or soft block: HTTP 429 code: BLOCKED fix: wait 30s for the circuit breaker to expire, or use --backend serpapi; do NOT retry immediately20 BLOCKED vs 21 SCHEMA_DRIFT: the pair that matters most
Section titled “20 BLOCKED vs 21 SCHEMA_DRIFT: the pair that matters most”These two look similar from the outside — both mean “the google backend didn’t return usable results” — but they mean opposite things about what to do next, and confusing them is the single most harmful mistake an agent driving roost can make.
20 BLOCKED means Google recognized the request as automated and pushed back: an HTTP 429,
an HTTP 403/503, a challenge page detected in the response body, or a consent interstitial
instead of results. roost’s response is to open its circuit breaker — recording the block and
its cooldown to $XDG_STATE_HOME/roost/ratelimit.json — so that further calls fail fast
locally instead of hammering an upstream that has already said no. The correct agent response
is to stop calling roost until the cooldown clears (roost doctor shows the remaining
time), or to switch to --backend serpapi. Retrying immediately doesn’t just fail again — it
deepens the block, since each 429/challenge extends the backoff schedule
(30s → 60s → 120s → 300s → 600s → 1800s) another step.
21 SCHEMA_DRIFT means the request actually succeeded — Google returned a normal 200 page —
but roost couldn’t parse it: the embedded protobuf data roost expects to find in the HTML no
longer has the shape roost’s parser was written for. This is not a transient condition and not
something the caller did wrong. It means Google changed its page format and roost’s parser is
now stale. The correct response is to upgrade roost (roost version --check), and if
already current, file an issue — retrying, waiting, or switching backends will not produce a
different result, because the code itself is out of date.
Collapsing these into one generic “search failed, try again” would be actively harmful in both directions: retrying into a BLOCKED state trains the upstream that roost ignores pushback (the opposite of the politeness roost is built around — see read-only and politeness), while treating SCHEMA_DRIFT as a wait-and-retry condition burns an agent’s retry budget on a call that will never succeed until the tool itself changes.
7 RATE is roost’s own throttle, not upstream
Section titled “7 RATE is roost’s own throttle, not upstream”Exit 7 (RATE_LIMITED) fires entirely client-side, before any request leaves the machine. It
means the local politeness throttle — minimum 6.0s between requests, a burst allowance of 2 —
hasn’t elapsed yet. This state persists across processes (roost is invoked fresh per call by an
agent, so an in-memory timer would be useless) at $XDG_STATE_HOME/roost/ratelimit.json.
By default roost fails fast rather than sleeping, because a hung CLI call can deadlock an agent loop:
$ roost search paris --jsonerror: throttled locally to stay polite; 4s until the next request is allowed code: RATE_LIMITED fix: wait 4s and retry, or pass --wait --max-wait 4Pass --wait (with --max-wait, default 30.0s) to have roost sleep through the throttle
instead of exiting; pass --no-throttle to skip it outright. Don’t confuse this with 20
BLOCKED — 7 means “you’re calling roost too fast for its own local pacing,” 20 means “Google
itself pushed back.” See politeness for why the throttle exists at all.
12 MUTATION_BLOCKED: defined but unreachable
Section titled “12 MUTATION_BLOCKED: defined but unreachable”roost defines MUTATION_BLOCKED (12) and ships the --allow-mutations flag alongside it, but
no command in roost ever raises it — roost is read-only end to end, with
no booking, reservation, or cancellation command of any kind. The flag and the exit code exist
purely for contract uniformity: roost is part of a fleet of CLIs built to the same agent-CLI
contract, and some of those tools do mutate. Keeping the same guard rail present — even inert —
means an agent driving multiple tools from that fleet sees one consistent surface instead of
having to special-case roost. In roost specifically, you will never see exit code 12 in
practice.
See also
Section titled “See also”- Read-only — why roost has no mutating commands at all
- Politeness — the throttle and circuit breaker behind codes 7 and 20
- Legitimacy — why roost stops on a block instead of evading it
- Output schema — the envelope shape for successful responses