Why roost cannot book
There is nothing to disable
Section titled “There is nothing to disable”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.
The inert flags, and why they ship anyway
Section titled “The inert flags, and why they ship anyway”roost still accepts --allow-mutations, --yes, and --force on every command:
roost search "kyoto" --check-in 2026-10-02 --nights 3 --allow-mutations --jsonThis 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.
Exit code 12: defined, unreachable
Section titled “Exit code 12: defined, unreachable”roost’s exit table includes 12 for MUTATION_BLOCKED:
0 ok 6 permission denied 13 input required1 generic error 7 rate limited 20 BLOCKED2 usage/parse 8 retryable 21 SCHEMA_DRIFT3 empty results 10 config error 130 cancelled4 auth required 12 mutation blocked (defined; unreachable — read-only tool)5 not founderrors.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.
What this means if you’re an agent
Section titled “What this means if you’re an agent”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.
Where booking would live, if it existed
Section titled “Where booking would live, if it existed”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.