for agents

Drive grr like a terminal tool, not an SDK.

grr is built to be called by something that is not a person: results on stdout, logs on stderr, JSON by default, and a discovery surface so you never have to guess whether a method exists. The same contract ships as a packaged agent skill.

stdout is data · stderr is noise · 402 methods discoverable
quickstart

The whole contract in one script.

Copy this into a harness bootstrap, or read the four rules under it. Everything here is copy-pasteable and every flag in it exists in the binary.

agent quickstart · bash
#!/usr/bin/env bash
# grr agent quickstart — the four rules that matter, in one script.
set -euo pipefail

# 1. Is there a usable credential? Exit status is the signal.
if ! grr auth status >/tmp/grr-auth.json 2>/tmp/grr-auth.err; then
  echo "not authenticated: $(cat /tmp/grr-auth.err)" >&2
  exit 1
fi
EMAIL=$(jq -r .email /tmp/grr-auth.json)

# 2. Discover, do not assume. Both work with no network and no login.
grr schema | jq -r '.subcommands[].name'
grr api list --service gmail --filter "messages" \
  | jq -r '.[].id'

# 3. Inspect a write before performing it. Sends nothing, prints JSON.
grr calendar events insert --calendar-id primary \
  --params '{"summary":"Standup","start":{"dateTime":"2026-10-05T09:30:00Z"},"end":{"dateTime":"2026-10-05T09:45:00Z"}}' \
  --dry-run | jq -e '.url' >/dev/null

# 4. Parse stdout only. stderr carries logs and human text. Gmail list
#    returns the API's wrapper object, so index into .messages first.
grr gmail users messages list --user-id me --q "from:github.com" --max-results 10 2>/dev/null \
  | jq -r '.messages[] | "\(.id)\t\(.threadId)"'
rule one

Parse stdout. Never stderr.

This is the single most important thing to get right, and grr is explicit about it in the code rather than leaving it to convention.

The tracing subscriber is wired to stderr at startup, deliberately, so grr … | jq cannot receive a log line. Data goes to stdout and only data goes to stdout.

A few commands annotate that contract in ways an agent should know about:

stdout vs stderr
$ grr gmail users messages list --user-id me --max-results 1
# stdout: {"messages":[{"id":"19f8ab2c","threadId":"19f8ab2c"}],"resultSizeEstimate":1}
# stderr: INFO grr_cli::core::auth: Loaded token from OS keyring

$ grr api list 2>&1 >/dev/null
402 methods                      # the count is deliberately on stderr

$ grr auth login --device
Visit https://google.com/device and enter code: ABCD-EFGH
# ^^ that line is plain text on STDOUT, before the JSON result

$ grr transport
# plain text, not JSON. Do not pipe this into a parser.
  • grr api list prints its method count to stderr and the array to stdout. That is why grr api list | jq is valid JSON.
  • grr auth login --device prints its human-readable URL and code to stdout with a plain println!, before the JSON result. If you are parsing the result of a device login, do not assume stdout is JSON for that one command.
  • grr transport prints a plain-text report, not JSON. It is a diagnostic for a human; read it, do not parse it.
  • grr skills install prints a plain-text summary of what it wrote (and exits non-zero when it refuses to clobber a locally edited file without --force). grr skills list is JSON, so inspect installation state with that.
  • Every --dry-run output is JSON, on either route (generated leaf or grr api call), so it pipes and a jq assertion works on it.
  • Suppress logs with 2>/dev/null when you want quiet failures, but keep them when you are debugging — the error text is where the useful detail is.

Errors are not JSON either. A failed command exits non-zero with a human-readable message on stderr, and that message is written to be actionable: a 403 names the missing scope, a missing path parameter names itself, an unknown method id comes with ranked suggestions.

Ask for JSON explicitly if you like being explicit

JSON is already the default, but -f json is harmless and documents intent at the call site. The four values are json, jsonl, table, and pretty.

Prefer -f json over -f table or -f pretty in anything automated. table is for humans, and its output is a rendered grid whose shape changes with the data. jsonl is the one to reach for when streaming a large list: one document per line, so you can process results incrementally instead of buffering the whole array.

There is no global --format flag, and that is not an oversight. A global flag would collide by id with per-method parameters named format — which is exactly why such parameters surface as --param-format.

rule two

Discover capabilities. Do not assume them.

The single biggest failure mode for an agent driving a Google CLI is confidently calling a method that does not exist, or a parameter that was renamed. Both are avoidable, and cheaply.

The discipline is fixed order: grr schema for the full command tree, grr api list to browse methods, grr api describe for one method’s parameters and scopes, --dry-run before the call. All four work with no network and no credential — the Discovery index is compiled into the binary — so an agent can answer “what can I do with Sheets?” before it has a login, which is exactly the order you want to fail in.

grr schema is the whole tree as JSON — every subcommand, every argument, whether it is required, its default, its possible values. Because the tree itself is generated from the Discovery index, schema is a faithful census of all 402 methods, and it answers with zero configuration. The naming rule means you rarely need it twice: a method id, dots turned into spaces, is the command (gmail.users.messages.list → grr gmail users messages list), and parameter names become kebab-case flags (userId → --user-id).

discovery · no network needed
# What can I do? No network, no credential needed.
$ grr api list --grouped | jq -r '.[] | "\(.service)\t\(.count)"'

# Narrow it. --filter matches ids AND descriptions, case-insensitively.
$ grr api list --filter spreadsheet | jq -r '.[].id'

# What does this one need? Params, required flags, scopes, verb, path.
$ grr api describe sheets.spreadsheets.values.update | jq '{
  http: .httpMethod,
  path: .path,
  required: [.parameters[] | select(.required) | .name],
  scope: .leastPrivilegeScope
}'

# The whole generated command tree, as a machine-readable contract.
$ grr schema | jq -r '.subcommands[].name'

Two habits worth building in:

  • Read describe before you construct a call. The parameters array tells you which are required, which are repeated, and which have enum values. Guessing an enum string is the most common cause of a 400.
  • Treat the method count as volatile. 402 methods is what the embedded index holds today; the index is refreshed daily from Google’s Discovery Service and the tree regenerates with it, so a method can appear or disappear between runs. Prefer --filter over a hard-coded list where you can, and handle the “unknown method” error as a normal outcome rather than a crash.
rule three

Inspect a request before you send it.

One dry-run surface, two routes into it — and both print JSON.

--dry-run exists on every generated leaf and on grr api call. It builds the request, validates the required parameters, and prints the verb, the fully resolved URL, the body, and the scopes — then sends nothing. URL construction and parameter validation both happen before the dry-run branch, so a dry run surfaces exactly the errors the real call would.

That makes the pattern “dry run, inspect the URL, then send” a cheap guard against the two failure modes an agent actually hits: a path parameter in the wrong place, and a body field Google rejects. A jq -e '.url' assertion on the dry-run output is a reasonable gate before a destructive call.

dry runs · destructive calls
# Every method — leaf or api call — has --dry-run. JSON out, nothing sent.
$ grr gmail users messages delete --user-id me --id 191f8ab2 --dry-run
{"method":"DELETE","url":"https://gmail.googleapis.com/gmail/v1/users/me/messages/191f8ab2",...}

# Prefer the reversible operation where the API offers one.
$ grr gmail users messages trash --user-id me --id 191f8ab2

# Bulk effect? Resolve the ids with a read first, show them, then act
# on the explicit list — never on a query you have not reviewed.
$ grr gmail users messages list --user-id me --q "from:newsletter.example" \
    --max-results 100 | jq -r '.messages[].id'

Note the asymmetry. messages trash is reversible; messages delete is not, and Gmail’s API has no “really delete” for the batch endpoints either. For anything destructive or bulk, resolve the ids first with a read, show them to the user, and only then act on the explicit list.

rule four

Know which scopes you actually hold.

grr requests a small default scope set at login — Gmail, Drive, Calendar, Tasks, Docs, Sheets, Slides, sign-in — and widens it per service on request. The narrowest scope each method accepts is reported per method; consent is still not per-method, so widening is a grr auth login --service <name> away rather than a runtime knob.

OAuth consent is granted at grr auth login for a small default set — Gmail, Drive, Calendar, Tasks, Docs, Sheets, Slides and the sign-in scopes — and widened with grr auth login --service <name> when a task needs more; --all-scopes asks for the whole index. You cannot change it from inside an agent run — a new consent needs a human — but the widening command is one line, and every login merges with the account’s existing grant. So scope is a design constraint, not a runtime knob, and the per-method scope grr reports is the thing to name when a call 403s.

Because every command resolves its method against the index, scope reporting is the same on both routes: read leastPrivilegeScope from grr api describe to see what a call touches, though nothing gates on it.

That leaves one failure mode to expect, and it is worth not misdiagnosing:

  • A 403 is the real answer, but not a verdict on the method. grr’s error names the scope it asked for and points at the remedy: the likely cause is a token that predates the current set, so re-run grr auth login to consent again — or the API is not enabled on the Cloud project. There is no note on stderr to watch for, and no method the index can express is refused for want of consent.

The full breakdown — how the set is derived, and the trade-off it carries — is on Discovery API.

credentials

You will never see a token, and you should not store one.

There is no flag that prints an access token. The credential lives in the OS keyring and grr reads it itself.

Tokens are stored in the operating system keyring — Windows Credential Manager, macOS Keychain, or the Linux Secret Service over D-Bus — under the service grr. On a headless machine with no keyring daemon, grr falls back to a token.json file under the user cache directory and imports it back into the keyring the next time one is available.

What an agent can observe is only the backend name, and only on login:

  • grr auth login prints token_backend (os-keyring or file) and a 20-character token_preview prefix. The preview exists so a human can confirm which credential was stored; it is not a usable token.
  • Refresh is automatic and transparent. A stored credential that has expired and has no refresh token errors with “rerun grr auth login” rather than silently opening a browser mid-call.
  • PKCE is always on, so there is no client secret in the request that matters, and nothing for an agent to capture from a network trace.

Do not attempt to read the token out of the keyring, and do not add a flag to expose it. If a workflow needs a token for something other than grr, that something should have its own OAuth client. grr auth setup writes a client to ~/.grr/config.toml (mode 0600 on unix) if you need to manage one non-interactively, and it refuses to hang on a non-TTY: pass --client-id and --client-secret, or it errors instead of prompting.

state

Check auth before you check anything else.

One cheap call, a real answer, and a history_id cursor worth keeping.

auth state
$ grr auth status
{"authenticated":true,"email":"you@example.com","messages_total":48213,
 "threads_total":9120,"history_id":"1280000"}

# Non-zero exit when there is no valid credential. Check this first in
# any automation: it is one cheap call and it fails loudly.
$ grr auth status >/dev/null || grr auth login --device

# history_id is the cursor for incremental reads.
$ grr gmail users history list --user-id me \
    --start-history-id 1280000 --max-results 100 | jq -r '.history[].id'

grr auth status makes a live Gmail users.getProfile request through the same shared call path as every other command, so it fails when the token is missing, revoked, or expired without a refresh token. Gate any automation on its exit status. grr auth login is interactive by default; --device is the only variant that works unattended, and it still needs a human to visit a URL and type a code.

For incremental reads, keep the history_id that auth status returns and feed it to grr gmail users history list --start-history-id on the next run. That is the difference between fetching a bounded window and re-reading the mailbox.

Failure modes worth handling explicitly

  • Not authenticated. Exit non-zero, message names the rerun. Do not retry in a loop.
  • Expired without a refresh token. The error says to rerun grr auth login. This happens if a token was stored without a refresh token; it is not transient.
  • HTTP 403 naming a scope. The scope is outside what the build consented to. Not fixable at runtime — report it rather than retrying.
  • Unknown method id. grr returns ranked suggestions. Treat it as “discover again”, not as a crash: the index is refreshed daily and the surface moves.
  • Missing required parameter. The message names the parameter and lists the others. Re-read grr api describe rather than retrying variations.
  • Empty results. A valid empty array, not an error. -f table renders it as a (no results) header; the JSON form is just [].
  • HTTP/2 fallback. grr transport showing fell_back: true means QUIC was blocked and HTTP/2 is in use. Nothing is wrong; do not retry or treat it as a fault.
mcp

Point the harness at grr mcp and it owns 402 tools.

A Model Context Protocol server over stdio: every method in the index as a typed tool, generated from the same Discovery index the CLI uses.

Register grr mcp as a stdio server in any MCP client (Claude Desktop, Gemini CLI, VS Code, Cursor, OpenClaw) and tools/list returns every one of the 402 methods with an inputSchema built from Discovery: typed properties, the required list, enum values, plus the params/body_file/query/dry_run escape hatches.

  • Tool name = the dotted method id (gmail.users.messages.list) — the same id grr api call takes, so a harness can switch between MCP and CLI without translating anything.
  • tools/call shares the engine with the CLI: results come back as JSON text content; errors are isError results and never kill the server.
  • --readonly gives a server that refuses every write method with an actionable error result — the right default when an agent works on a user’s behalf.
  • Protocol revision: the stateless 2026-07-28 one — call tools/* inline and server/discover reports supportedVersions plus the server identity, which rides in every result’s _meta. A minimal legacy initialize handshake is also answered, so standard MCP clients that still open with it connect instead of erroring.

Google now ships its own remote MCP servers for Workspace (Gmail, Calendar, Drive, People, Chat on *mcp.googleapis.com, developer preview) plus 50+ Google Cloud ones. Those suit a harness that can hold a remote endpoint per service and a Cloud project with the APIs enabled and consent configured. grr mcp is the local alternative: one stdio process covering every API in the index, offline discovery, the CLI’s safety profiles, and nothing to deploy.

natural language

grr ask — plain English in, a typed plan out.

A System One model (Jev by default, provider-agnostic by config) classifies the request hierarchically and fills the typed parameters, from the same 402-method catalog.

grr ask "show my unread gmail" classifies service (14 options), then method, then one typed question per required parameter — every candidate is code-supplied from the discovery index, so the model can only select, never invent.

  • It prints the plan by default (method, params, url, scopes, confidence as JSON) and sends nothing to Google; the routing judgments themselves are calls to the System One provider. --run executes through the same engine as grr api call.
  • Confidence is visible: below the threshold (default 0.6) the plan is flagged, not hidden — a harness can decide whether to proceed or ask the user.
  • The safety gate applies here too: --readonly/--deny-service fire as soon as the method is resolved, before any judgment on parameters.

Endpoint, model, API key and confidence threshold are the one tunable surface in grr; the System One setup on the Commands page covers the config file and the preferred TYPESAFE_API_KEY env route.

Mapping this onto a harness

Claude Code, Codex, Cursor, Gemini CLI, OpenClaw, and anything else that shells out can be set up from the same sources, in this order:

  • grr skills install — writes the packaged core skill plus the 14 per-service skills into ~/.agents/skills/, offline and globally (never a project directory); --claude mirrors into ~/.claude/skills/ for Claude Code, which reads nothing else. grr skills list reports the state of each target.
  • skills/grr/SKILL.md ↗ — the packaged agent skill: the discovery-first discipline, the naming rule, and the output contract in one file. Install it with npx skills add https://github.com/debanjanbasu/grr-cli.
  • llms.txt — the site’s machine-readable index, for orientation. Fetch it once at session start.
  • grr schema — the generated command tree, for building tool definitions. Regenerate it rather than hand-writing flags.
  • The terminal demo — 80 seconds of real grr output, including an opencode session driving grr the way a harness would.
  • grr api list and grr api describe — the method-level view of the same index, at runtime, with no network and no credential.

The quickstart at the top of this page is the portable form of the same guidance — paste it into any harness’s system prompt or bootstrap script, or install the skill and let the harness load it.