Skip to content

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.

CodeNameMeaningWhat the caller should do
0OKThe command completed.Read the output.
1GENERICAn error that doesn’t fit a more specific code (e.g. an unexpected upstream HTTP status).Log it, don’t blind-retry.
2USAGEBad 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.
3EMPTYThe 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.)
4AUTHThe serpapi backend needs a key that isn’t set.roost auth login --token-stdin, or set ROOST_SERPAPI_KEY.
5NOT_FOUNDA 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.
6PERMA 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.
7RATEroost’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.
8RETRYA transient failure — network error, upstream 5xx.Retry shortly; it’s reasonable to retry this one.
10CONFIGLocal 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.
12MUTATION_BLOCKEDDefined for contract uniformity; unreachable in roost.N/A — see below.
13INPUT_REQUIREDA value was needed but neither supplied as a flag nor promptable (--no-input is set).Pass the value explicitly as a flag or argument.
20BLOCKEDGoogle served a challenge, consent interstitial, or soft block.Stop. Wait for the circuit breaker to expire, or switch --backend serpapi. Do not retry immediately.
21SCHEMA_DRIFTThe upstream response didn’t parse the way roost expects.Upgrade roost (roost version --check); if already current, file an issue. Retrying will not help.
130CANCELLEDThe user or environment interrupted the command (Ctrl-C).No action; this wasn’t a tool failure.

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 immediately

20 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:

Terminal window
$ roost search paris --json
error: 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 4

Pass --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.

  • 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