Skip to content

Why roost cannot book

Most CLIs that talk about being “read-only” mean a runtime gate: a mutating code path exists, and a flag decides whether to let it run. roost isn’t built that way. It has three commands that touch the network — search, rates, dates — and all three end in a GET-style fetch of a Google Hotels page or a SerpApi search. There is no book, reserve, hold, or cancel command, no payment field anywhere in the client, and no code path that would submit a reservation even if you wanted it to. You can’t misconfigure your way into a booking, because the function that would place one was never written.

That’s a stronger guarantee than a policy check. A policy check can have a bug. A capability that doesn’t exist can’t.

roost still accepts --allow-mutations, --yes, and --force on every command:

Terminal window
roost search "kyoto" --check-in 2026-10-02 --nights 3 --allow-mutations --json

This runs exactly the same search as without the flag. From cli.py, here’s the entire implementation of the mutation gate those flags feed:

def guard(self, op: str) -> None:
"""Mutation gate. Present for contract uniformity — roost has no mutations, so
nothing calls this. Kept so the surface stays identical across the fleet."""
if not self.allow_mutations:
from .errors import mutation_blocked
raise mutation_blocked(op)

guard() exists, but nothing in roost ever calls it, because nothing in roost ever mutates anything. --allow-mutations is stored on the runtime and never read again. --yes and --force are accepted and otherwise ignored — there’s no confirmation prompt to skip and no safety check to bypass.

Why ship flags that do nothing? Because roost is one tool in a fleet of agent CLIs built to the same contract, and an agent driving several of them shouldn’t need to know in advance which ones can hurt it. If you’re scripting a loop over roost, gfly, and half a dozen other tools and one of them does have a real mutating command behind --allow-mutations, you want that flag to be a no-op everywhere it doesn’t apply, not a parse error. Omitting the flag would save nothing — an agent still has to check whether it’s supported before deciding whether to pass it, which is worse than it always being supported and always being safe.

roost’s exit table includes 12 for MUTATION_BLOCKED:

0 ok 6 permission denied 13 input required
1 generic error 7 rate limited 20 BLOCKED
2 usage/parse 8 retryable 21 SCHEMA_DRIFT
3 empty results 10 config error 130 cancelled
4 auth required 12 mutation blocked (defined; unreachable — read-only tool)
5 not found

errors.py defines the error it would raise:

def mutation_blocked(op: str) -> AppError:
return AppError(
ExitCode.MUTATION_BLOCKED,
"MUTATION_BLOCKED",
f"{op} is a mutating operation and is blocked by default",
"re-run with --allow-mutations (add --dry-run to preview)",
)

That function is real, tested, and dead — its only caller is guard(), and guard() is never invoked. No command in roost will ever exit 12. It stays in the table for the same reason the flags stay in the CLI: an agent parsing roost schema --json sees the full, stable exit code space the fleet contract defines, whether or not this particular tool exercises every code in it. roost schema --json also states the read-only posture directly, in a safety block:

{
"safety": {
"allow_mutations": false,
"dry_run": false,
"no_input": false,
"read_only": true,
"note": "roost has no mutating operations; it never books or transacts"
}
}

An agent that wants to confirm roost can’t mutate anything doesn’t have to trust this page — it can read that block at runtime.

You can run roost unattended without a human in the loop worrying about accidental spend. There is no sequence of flags, arguments, or retries that results in a reservation, a charge, or any state change on Google’s or SerpApi’s side — every request roost makes is a search. That also means the usual mutation-safety choreography doesn’t apply here: no --dry-run before the real run (--dry-run on roost only ever affects dates, where it previews the request plan, not a write — see dates), no --yes to confirm, no --allow-mutations to unlock anything. Passing them is harmless, but it’s also not doing what it would do on a tool that has a real mutating command behind the same names. If an exit code ever pushes back on a call, it’s one of the read-only failure modes — throttled (politeness), blocked, empty, not found — never a mutation guard.

roost is one of a family of agent CLIs built against the same contract, each scoped to a single upstream and a single job. Its sibling tools search flights and short-term rentals the same way: read availability and price, and stop there. If booking were ever built for hotels, it would not be bolted onto roost as a fourth command behind --allow-mutations — it would be a separate tool, with its own credential model (booking requires an authenticated account and a payment method; searching requires neither), its own confirmation flow, and its own audit trail. Keeping search and booking as different tools means the read-only guarantee in this page doesn’t have an asterisk: you never have to check which invocation of roost is safe to run unattended, because all of them are.

See legitimacy for the boundary on what roost is allowed to read in the first place, and how it works for how a search request is built and throttled.