Every service command is generated from Google’s Discovery Service at build time — 402 methods, a typed flag per parameter, zero hand-written per-service commands. That makes the tree predictable instead of memorized. For the flat, id-based view of the same index, see Discovery API.
402 methods · 14 services · generated, not curated
the naming rule
Predict any command from its method id.
There is nothing to memorize: a Discovery method id, with dots turned into spaces, is the command. The same rule generates every flag.
method id → command
# A method id, with dots turned into spaces, IS the command.
gmail.users.messages.list grr gmail users messages list
calendar.events.list grr calendar events list
drive.files.list grr drive files list
tasks.tasklists.list grr tasks tasklists list
sheets.spreadsheets.values.get grr sheets spreadsheets values get
# Resources nest as subcommands; the method is the leaf. Each leaf also
# carries its bare name as a visible alias, shown in --help.
grr gmail --help # "users" and its children, alphabetically
grr gmail users --help # "messages", "drafts", "labels", ...
# camelCase method names keep their casing as aliases:
grr gmail users getProfile --user-id me
Top level: grr auth, grr api, grr mcp, grr schema, grr completions, grr transport, grr ask, grr skills, plus one subcommand per service. The tree is emitted into src/commands/generated.rs by a generator that reads the committed Discovery index, and a daily workflow opens a PR when Google adds a method — so new surface arrives without a grr release.
Flag names come from the same ids, camelCase collapsed to kebab-case, typed by what Discovery declares:
Integers are parsed as numbers: --max-results 10.
Booleans are presence flags: --include-spam-trash, not --include-spam-trash true.
Repeated parameters repeat: --label-ids INBOX --label-ids UNREAD. (Single-string parameters that take comma lists inside the value, like Drive’s --order-by, are passed through as-is.)
Enum parameters validate their values; a bad one is caught before anything is sent.
Required parameters are enforced by clap — you cannot build a request missing --user-id.
Bodies are untyped, deliberately. Discovery’s request schemas are not part of the distilled index, so POST/PATCH/PUT bodies pass through --params (a JSON object) or --body-file (a file, or - for stdin) verbatim. Typed flags win over --params on conflict.
typed flags · collision rule · bodies
# Flags are generated from Discovery parameter names: camelCase -> kebab-case.
grr gmail users messages list --user-id me \
--q "is:unread" \
--max-results 5 \
--label-ids INBOX --label-ids UNREAD \
--include-spam-trash
# --max-results is parsed as an integer, --include-spam-trash is a presence
# flag, --label-ids repeats. Enum parameters validate; required ones are
# enforced before anything is sent.
# Collision rule: a parameter literally named "format" or "query" becomes
# --param-format / --param-query (the -f output flag keeps --format).
grr gmail users messages get --user-id me --id <id> --param-format metadata
# Bodies are untyped by design -- Discovery request schemas are not in the
# index -- so they pass through --params or --body-file verbatim.
grr calendar events insert --calendar-id primary --body-file ./event.json
Every leaf also carries the same escape hatches grr api call has: --params, --body-file, repeatable --query KEY=VALUE for raw query pairs, --dry-run, and -f/--format. Dispatch resolves the leaf’s id against the embedded index and funnels into one shared call path — the two routes below produce byte-identical output:
piping · dry runs · parity
# stdout is data, stderr is logs -- so this is always valid JSON.
$ grr gmail users messages list --user-id me --q "from:github.com" --max-results 10 | jq -r '.messages[0].id'
19f8ab2c
# --dry-run prints the resolved request as JSON and sends nothing.
$ grr gmail users messages list --user-id me --q "is:unread" --dry-run | jq -r '.url'
https://gmail.googleapis.com/gmail/v1/users/me/messages?q=is%3Aunread
# The same method, reached through the flat escape hatch, byte-identical:
$ grr api call gmail.users.messages.list --param userId=me --param 'q=is:unread' --dry-run
the services
Every service, a few real commands.
Verified against the binary and the committed index. Run grr <service> --help for the authoritative version of any branch — these are samples, not the census.
Gmail
79 methods. Messages, threads, drafts, labels, history, attachments, filters, forwarding, POP/IMAP, send-as, CSE, delegates, and watches — the deepest tree.
grr gmail
grr gmail users messages list --user-id me --q "in:inbox" --max-results 5
grr gmail users messages get --user-id me --id <message-id> --param-format metadata
grr gmail users messages attachments get --user-id me --message-id <message-id> --id <attachment-id>
grr gmail users threads list --user-id me --max-results 10
grr gmail users labels list --user-id me
grr gmail users drafts create --user-id me --body-file ./draft.json
grr gmail users settings filters list --user-id me
Calendar
38 methods. Calendars, events, recurring instances, ACL, free/busy, colors, and settings. The calendar id is a parameter like any other — primary where one is accepted.
grr calendar
grr calendar events list --calendar-id primary --max-results 10
grr calendar events get --calendar-id primary --event-id <event-id>
grr calendar events insert --calendar-id primary --body-file ./event.json
grr calendar calendarList list
grr calendar freebusy query --body-file ./freebusy.json
grr calendar colors get
Drive
64 methods. Files, permissions, comments, replies, revisions, changes, drives, and approvals — including the long tail no CLI ever wraps by hand.
grr drive
grr drive files list --q "name contains 'report'" --page-size 10
grr drive files get --file-id <file-id>
grr drive files export --file-id <document-id> --mime-type application/pdf
grr drive permissions list --file-id <file-id>
grr drive comments list --file-id <file-id>
grr drive changes list --page-token <token>
grr drive files emptyTrash
People
24 methods. The People API verbatim: resource names look like people/c12345, and the API nests one people level, so the command says it twice.
grr people
grr people people connections list --resource-name people/me
grr people people get --resource-name people/c12345
grr people contactGroups list
grr people otherContacts list
Chat
55 methods. Spaces, members, messages, reactions, media, custom emoji, and read state. Space-scoped methods take --parent spaces/… .
grr chat
grr chat spaces list --page-size 10
grr chat spaces messages create --parent spaces/AAAA --body-file ./message.json
grr chat spaces messages list --parent spaces/AAAA --page-size 25
grr chat spaces members list --parent spaces/AAAA
Forms
10 methods. Form bodies, responses, publish settings, and push-notification watches. The API nests one forms level, like People does with people.
grr forms
grr forms forms get --form-id <form-id>
grr forms forms responses list --form-id <form-id>
grr forms forms watches list --form-id <form-id>
Tasks
14 methods. Task lists and the tasks inside them. Note the scope caveat: this build does not consent to the Tasks scope, so live calls can 403.
grr tasks
grr tasks tasklists list
grr tasks tasks list --tasklist @default
grr tasks tasks insert --tasklist @default --body-file ./task.json
Sheets
17 methods. Spreadsheets, values (read, write, append, batch), and developer metadata. Colon-verb paths like values/{range}:append are handled for you.
Every command takes -f/--format, including the generated leaves. There is no global --format flag: it would collide by id with per-method parameters named format.
json (default), jsonl, table, pretty. JSON is the default because the common consumer is a pipe, not a person.
jsonl emits one JSON document per line for array results, which is what you want when streaming into jq without loading everything. table renders arrays as one row per element with the union of keys as columns, a single object as a Field/Value table, and nested values as compact JSON inside a cell.
output formats
# Every command accepts -f/--format. JSON is the default. Google's list
# methods return their own wrapper object -- messages, files, items -- and
# grr prints it verbatim rather than unwrapping it, so table shows the
# object Field/Value and jsonl emits that one object:
$ grr gmail users messages list --user-id me --max-results 5 -f table
+--------------------+-------------------------------------------------+
| Field | Value |
+--------------------+-------------------------------------------------+
| messages | [{"id":"19f8ab2c","threadId":"19f8ab2c"}, ...] |
| resultSizeEstimate | 5 |
+--------------------+-------------------------------------------------+
$ grr gmail users messages list --user-id me -f jsonl
{"messages":[{"id":"19f8ab2c","threadId":"19f8ab2c"}],"resultSizeEstimate":1}
# grr api list is a bare array, so it tables one row per method and streams
# one element per line -- the shape jsonl is actually for:
$ grr api list --service docs -f jsonl
{"http":"POST","id":"docs.documents.batchUpdate","summary":"Applies one or more updates to the document."}
{"http":"POST","id":"docs.documents.create","summary":"Creates a blank document using the title given in the request."}
{"http":"GET","id":"docs.documents.get","summary":"Gets the latest version of the specified document."}
$ grr api list --service docs -f pretty
account level
Commands outside the service tree.
These sit at the top level because they are not about one API. grr schema is the one to build against.
grr auth
login runs the PKCE browser flow, or the device flow with --device; --account work keeps a separate token per account. status makes a live profile call through the shared path and reports the account plus the history_id cursor. setup creates and stores your own OAuth client.
grr transport
Prints the negotiated protocol, whether HTTP/3 was requested and effective, whether it fell back, the core count, and io_uring support. Plain text, not JSON — it is a diagnostic, not data.
grr schema
Dumps the entire command tree as JSON: every subcommand, every argument, whether it is required, whether it takes a value, its default, and its possible values. Because the tree itself is generated from the index, schema is a faithful census of all 402 methods.
grr completions
Prints a bash, zsh, PowerShell or elvish completion script generated from the same tree as schema, so every service command, alias and flag completes offline with zero configuration. Bash and zsh load installed scripts lazily; PowerShell and elvish snippets run the generator at shell startup. Fish is not supported yet: its generator stops two levels deep. Setup per shell →
grr api
list, describe, call, and refresh against the embedded Discovery index — the flat escape hatch that reaches any method by id. Same index, same call path as the tree.
grr skills
install writes the packaged skills — the core skill plus one per service, 16 files with their index — into ~/.agents/skills/, the cross-client user-level location, with no network; --claude mirrors into ~/.claude/skills/, which is all Claude Code reads. Identical files are a no-op, a locally edited file is refused without --force. list reports what each target holds.
grr mcp
A Model Context Protocol server over stdio: every method as a typed MCP tool with an inputSchema from Discovery. tools/call shares the engine with grr api call; --readonly refuses writes. Point Claude Desktop, Gemini CLI, VS Code or Cursor at it.
grr ask
Natural language in, a typed method + parameters out: a System One model (Jev by default) classifies the request hierarchically — service, then method, then one typed question per required parameter, every candidate code-supplied. Prints the plan to Google; --run executes. Configure System One →
grr schema is the canonical reference. It is honest about its own size: roughly 369 KB of valid JSON, emitted in well under 100 ms, with zero configuration and before any OAuth exists. Generate a wrapper or an agent tool definition from it — because it is derived from the same tree the binary dispatches on, it cannot drift from the commands that actually exist.
The same zero-configuration promise covers the packaged agent skills: they are compiled into the binary, so grr skills install writes the whole set into the user-level skills directory with no network — global only, never a project directory.
grr skills · offline install
# The 15 packaged skills -- the core skill plus one per service -- are 16 files
# with their index, and are compiled into the binary, so install is offline.
$ grr skills install # all of them into ~/.agents/skills/ (the cross-client location)
$ grr skills install --claude # also mirror into ~/.claude/skills/ (all Claude Code reads)
$ grr skills install --force # replace files that differ from the packaged copy
$ grr skills list # the packaged set, and what each target directory holds
# on upgrade, installed skills migrate themselves: untouched files are
# replaced, locally edited files are 3-way merged, conflicts land beside
# yours as .grr-incoming (lazy — nothing is hashed unless a merge is due)
natural language
Configuring System One for grr ask.
grr ask is the one command with a tunable surface: endpoint, model, key and confidence threshold. Code chooses the candidates; the model only selects among them.
grr ask runs three small judgments against a System One provider, each one HTTP request: a Choice over the 14 services, then a Choice over the chosen service’s methods, then one typed question per required parameter. The CLI supplies every candidate from the embedded Discovery index, so the model can only select a service, a method, or a value it was handed — it cannot invent one. The plan is the default output; --run executes the resolved request through the same call path as grr api call.
The defaults point at TypeSafe’s hosted endpoint and its Jev model, so the only thing you must supply is a key. The provider is agnostic: any endpoint that speaks the same contract — POST a state plus typed questions, answer with an answers map — works by pointing endpoint and model at it.
~/.grr/config.toml · [systemone]
# ~/.grr/config.toml -- every key is optional; the defaults are shown.
[systemone]
endpoint = "https://api.typesafe.ai/v1/systemone"
model = "jev-latest"
confidence-threshold = 0.6
# api-key = "..." # avoid: prefer TYPESAFE_API_KEY so the key stays out of the file
Keys are kebab-case (api-key, confidence-threshold); snake_case aliases keep TS-era configs working, and any field left unset falls back to its default. The env route is preferred for the key, because a key in a config file can leak with the file:
the preferred key route
# Preferred key route: it never sits in a file that can be committed.
export TYPESAFE_API_KEY="..."
grr ask "show my unread messages" # prints the plan, sends nothing to Google
grr ask "show my unread messages" --run # executes the resolved request
# The generic GRR_ prefix reaches the rest of the section too:
# GRR_SYSTEMONE__ENDPOINT, GRR_SYSTEMONE__MODEL, GRR_SYSTEMONE__API_KEY
What the model decides: which service, which method, and which value for each required parameter — always a choice among code-supplied options — plus a confidence for each.
What the CLI decides: the candidate sets, the parameter types and coercion, the scope note, and the safety gate. --readonly/--deny-service/--deny-verb fire the moment the method resolves, before the parameter judgment. A choice confidence below the threshold (default 0.6) is flagged on stderr, never blocked.
The key rides only in the Authorization header — never in a request body, the plan output, or an error message. A missing key fails with the ways to supply one rather than a bare 401.
Planning still needs the provider. Even without --run, the routing judgments are HTTP calls, so grr ask needs a reachable endpoint and a valid key; --service skips the first judgment and --method skips both, going straight to parameter fill.
Safety profiles, everywhere at once
--readonly, --deny-service (a service name) and --deny-verb (an HTTP verb) are global flags honored by the generated tree, grr api call and the MCP server alike. They parse before or after the subcommand, and turn a destructive method into an actionable error naming the method, the verb, and the way out.
safety profiles
# --readonly refuses every write verb, before or after the subcommand
grr --readonly gmail users messages delete --user-id me --id abc
# -> Error: 'gmail.users.messages.delete' is a DELETE (write); --readonly refuses writes.
grr --deny-service chat chat spaces list
# -> Error: 'chat.spaces.list' belongs to service chat, which --deny-service refuses.
# reads are never refused
grr --readonly gmail users messages list --user-id me --dry-run # runs, JSON out
Conventions worth knowing
Results on stdout, logs on stderr. The tracing subscriber is wired to stderr explicitly, so grr … | jq never receives a log line. The one stdout exception is grr auth login --device, which prints its human-readable URL and code before the final JSON.
Paging is yours to drive. The tree does not follow page tokens: --max-results is the API’s own page size, and --page-token takes the nextPageToken from the previous response. That keeps a list command from turning into an unbounded crawl.
Booleans are presence flags (--include-spam-trash), a deliberate change from value-taking booleans: a flag is either passed or absent, so a script never has to guess.
Errors are actionable. A 403 names the missing scope, an unknown method id returns ranked suggestions, a missing required parameter names itself. Non-zero exit on failure, always.
Three commands print plain text, not JSON:grr transport, grr skills install (a human install summary), and the device-login line above. grr skills list is JSON. --dry-run output is JSON, so it pipes.
Timestamps are RFC 3339 (--time-min 2026-10-01T00:00:00Z), or a bare YYYY-MM-DD where the API accepts a date.
grr --version is a banner. The semver is its first line, which is what the benchmark script and the Homebrew formula read; below it sits the crab mascot, drawn in 24-bit-colour pixel art on a terminal that advertises COLORTERM or direct colour and as plain ASCII everywhere else. A pipe, TERM=dumb or NO_COLOR all give you the ASCII crab — so scripts are never handed escape sequences. CLICOLOR_FORCE=1 forces the colour logo through a pipe.
Where the tree comes from
Every leaf above is generated from the committed Discovery index at build time and refreshed daily by a workflow PR. The same index drives the flat grr api surface, so the census never splits in two.