grr-cli 0.11.3

Google tools from the terminal, at maximum performance: 401 methods across 14 Google APIs in one generated command tree, zero-config, over HTTP/3
Documentation
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import DocsShell from '../../components/DocsShell.astro';
import CodeBlock from '../../components/CodeBlock.astro';
import PageHero from '../../components/PageHero.astro';
import SectionIntro from '../../components/SectionIntro.astro';
import { demoDurationSeconds } from '../../lib/demo-duration';
import { discoveryMethodTotal, discoveryServiceCount } from '../../data/discovery-coverage';

const base = import.meta.env.BASE_URL;

const quickstart = `#!/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)"'`;
const contract = `$ 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
${discoveryMethodTotal} 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.`;
const discover = `# 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'`;
const 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'`;
const safe = `# 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'`;
---

<BaseLayout
  title="Agents and harnesses"
  description="How an AI agent or harness should drive grr: parse stdout not stderr, discover with grr schema and grr api list instead of assuming, dry-run writes, respect scopes, and use the packaged agent skill."
>
  <DocsShell currentPath={`${base}docs/agents/`}>
    <PageHero eyebrow="for agents" title="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 <a href="https://github.com/debanjanbasu/grr-cli/blob/main/skills/grr/SKILL.md" target="_blank" rel="noreferrer">agent skill</a>.
      <Fragment slot="aside">stdout is data · stderr is noise · {discoveryMethodTotal} methods discoverable</Fragment>
    </PageHero>

    <section class="section" aria-labelledby="quickstart-title">
      <div class="container">
        <SectionIntro
          id="quickstart-title"
          eyebrow="quickstart"
          title="The whole contract in one script."
          copy="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."
        />
        <div class="container prose reveal max-w-[68ch]">
          <CodeBlock code={quickstart} label="agent quickstart · bash" />
        </div>
      </div>
    </section>

    <section class="section" aria-labelledby="stdout-title">
      <div class="container">
        <SectionIntro
          id="stdout-title"
          eyebrow="rule one"
          title="Parse stdout. Never stderr."
          copy="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."
        />
        <div class="container prose reveal max-w-[68ch]">
          <p class="lead">The tracing subscriber is wired to stderr at startup, deliberately, so <code>grr … | jq</code> cannot receive a log line. Data goes to stdout and only data goes to stdout.</p>
          <p>A few commands annotate that contract in ways an agent should know about:</p>
          <CodeBlock code={contract} label="stdout vs stderr" />
          <ul>
            <li><code>grr api list</code> prints its method count to stderr and the array to stdout. That is why <code>grr api list | jq</code> is valid JSON.</li>
            <li><code>grr auth login --device</code> prints its human-readable URL and code to <strong>stdout</strong> with a plain <code>println!</code>, before the JSON result. If you are parsing the result of a device login, do not assume stdout is JSON for that one command.</li>
            <li><code>grr transport</code> prints a plain-text report, not JSON. It is a diagnostic for a human; read it, do not parse it.</li>
            <li><code>grr skills install</code> prints a plain-text summary of what it wrote (and exits non-zero when it refuses to clobber a locally edited file without <code>--force</code>). <code>grr skills list</code> is JSON, so inspect installation state with that.</li>
            <li>Every <code>--dry-run</code> output is JSON, on either route (generated leaf or <code>grr api call</code>), so it pipes and a <code>jq</code> assertion works on it.</li>
            <li>Suppress logs with <code>2>/dev/null</code> when you want quiet failures, but keep them when you are debugging &mdash; the error text is where the useful detail is.</li>
          </ul>
          <p>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.</p>
        </div>
      </div>
    </section>

    <section class="section" aria-labelledby="format-title">
      <div class="container prose reveal">
        <h2 id="format-title">Ask for JSON explicitly if you like being explicit</h2>
        <p>JSON is already the default, but <code>-f json</code> is harmless and documents intent at the call site. The four values are <code>json</code>, <code>jsonl</code>, <code>table</code>, and <code>pretty</code>.</p>
        <p>Prefer <code>-f json</code> over <code>-f table</code> or <code>-f pretty</code> in anything automated. <code>table</code> is for humans, and its output is a rendered grid whose shape changes with the data. <code>jsonl</code> 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.</p>
        <p>There is no global <code>--format</code> flag, and that is not an oversight. A global flag would collide by id with per-method parameters named <code>format</code> &mdash; which is exactly why such parameters surface as <code>--param-format</code>.</p>
      </div>
    </section>

    <section class="section" aria-labelledby="discover-title">
      <div class="container">
        <SectionIntro
          id="discover-title"
          eyebrow="rule two"
          title="Discover capabilities. Do not assume them."
          copy="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."
        />
        <div class="container prose reveal max-w-[68ch]">
          <p class="lead">The discipline is fixed order: <code>grr schema</code> for the full command tree, <code>grr api list</code> to browse methods, <code>grr api describe</code> for one method&rsquo;s parameters and scopes, <code>--dry-run</code> before the call. All four work with no network and no credential &mdash; the Discovery index is compiled into the binary &mdash; so an agent can answer &ldquo;what can I do with Sheets?&rdquo; before it has a login, which is exactly the order you want to fail in.</p>
          <p><code>grr schema</code> is the whole tree as JSON &mdash; 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 {discoveryMethodTotal} 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 (<code>gmail.users.messages.list</code> &rarr; <code>grr gmail users messages list</code>), and parameter names become kebab-case flags (<code>userId</code> &rarr; <code>--user-id</code>).</p>
          <CodeBlock code={discover} label="discovery · no network needed" />
          <p>Two habits worth building in:</p>
          <ul>
            <li><strong>Read <code>describe</code> before you construct a call.</strong> The <code>parameters</code> array tells you which are <code>required</code>, which are <code>repeated</code>, and which have <code>enum</code> values. Guessing an enum string is the most common cause of a 400.</li>
            <li><strong>Treat the method count as volatile.</strong> {discoveryMethodTotal} methods is what the embedded index holds today; the index is refreshed daily from Google&rsquo;s Discovery Service and the tree regenerates with it, so a method can appear or disappear between runs. Prefer <code>--filter</code> over a hard-coded list where you can, and handle the &ldquo;unknown method&rdquo; error as a normal outcome rather than a crash.</li>
          </ul>
        </div>
      </div>
    </section>

    <section class="section" aria-labelledby="dryrun-title">
      <div class="container">
        <SectionIntro
          id="dryrun-title"
          eyebrow="rule three"
          title="Inspect a request before you send it."
          copy="One dry-run surface, two routes into it — and both print JSON."
        />
        <div class="container prose reveal max-w-[68ch]">
          <p class="lead"><code>--dry-run</code> exists on every generated leaf and on <code>grr api call</code>. It builds the request, validates the required parameters, and prints the verb, the fully resolved URL, the body, and the scopes &mdash; 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.</p>
          <p>That makes the pattern &ldquo;dry run, inspect the URL, then send&rdquo; 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 <code>jq -e '.url'</code> assertion on the dry-run output is a reasonable gate before a destructive call.</p>
          <CodeBlock code={safe} label="dry runs · destructive calls" />
          <p>Note the asymmetry. <code>messages trash</code> is reversible; <code>messages delete</code> is not, and Gmail&rsquo;s API has no &ldquo;really delete&rdquo; 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.</p>
        </div>
      </div>
    </section>

    <section class="section" aria-labelledby="scope-title">
      <div class="container">
        <SectionIntro
          id="scope-title"
          eyebrow="rule four"
          title="Know which scopes you actually hold."
          copy="grr consents to one fixed set of sixteen scopes. The narrowest scope each method accepts is reported per method; consent itself is not per-method."
        />
        <div class="container prose reveal max-w-[68ch]">
          <p class="lead">OAuth consent is granted once, for a fixed set of scopes, at <code>grr auth login</code>. You cannot widen it later from inside an agent run &mdash; that needs a new consent, which needs a human. So scope is a design constraint, not a runtime knob, and the per-method scope grr reports is a heads-up, not a request it can grant.</p>
          <p>Because every command resolves its method against the index, the scope check is the same on both routes: read <code>leastPrivilegeScope</code> from <code>grr api describe</code> before relying on a method, and treat the result as part of your plan, not an afterthought.</p>
          <p>Two behaviours to expect, both benign, both worth not misdiagnosing:</p>
          <ul>
            <li>A <strong>note on stderr</strong> saying a method wants a scope outside the consented set is a warning, not a verdict. grr attempts the call anyway, because the narrow scope is often a sibling of a broader one the build does hold. Check the exit status, not the note.</li>
            <li>A <strong>403</strong> is the real answer. grr&rsquo;s error names the specific scope and the file to edit. The common cause is a method genuinely outside the consented set &mdash; Gmail CSE keypairs, S/MIME settings, directory people, most of the Chat admin surface, and the five services whose methods never accept a consented scope: Tasks, Analytics Admin, Analytics Data, Search Console, and Apps Script except <code>scripts.run</code> (which accepts the Drive scope).</li>
          </ul>
          <p>The full breakdown, including which services work and which do not, is on <a href={`${base}docs/discovery/`}>Discovery API</a>.</p>
        </div>
      </div>
    </section>

    <section class="section" aria-labelledby="creds-title">
      <div class="container">
        <SectionIntro
          id="creds-title"
          eyebrow="credentials"
          title="You will never see a token, and you should not store one."
          copy="There is no flag that prints an access token. The credential lives in the OS keyring and grr reads it itself."
        />
        <div class="container prose reveal max-w-[68ch]">
          <p class="lead">Tokens are stored in the operating system keyring &mdash; Windows Credential Manager, macOS Keychain, or the Linux Secret Service over D-Bus &mdash; under the service <code>grr</code>. On a headless machine with no keyring daemon, grr falls back to a <code>token.json</code> file under the user cache directory and imports it back into the keyring the next time one is available.</p>
          <p>What an agent can observe is only the backend name, and only on login:</p>
          <ul>
            <li><code>grr auth login</code> prints <code>token_backend</code> (<code>os-keyring</code> or <code>file</code>) and a 20-character <code>token_preview</code> prefix. The preview exists so a human can confirm <em>which</em> credential was stored; it is not a usable token.</li>
            <li>Refresh is automatic and transparent. A stored credential that has expired and has no refresh token errors with &ldquo;rerun <code>grr auth login</code>&rdquo; rather than silently opening a browser mid-call.</li>
            <li>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.</li>
          </ul>
          <p>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. <code>grr auth setup</code> writes a client to <code>~/.grr/config.toml</code> (mode <code>0600</code> on unix) if you need to manage one non-interactively, and it refuses to hang on a non-TTY: pass <code>--client-id</code> and <code>--client-secret</code>, or it errors instead of prompting.</p>
        </div>
      </div>
    </section>

    <section class="section" aria-labelledby="state-title">
      <div class="container">
        <SectionIntro
          id="state-title"
          eyebrow="state"
          title="Check auth before you check anything else."
          copy="One cheap call, a real answer, and a history_id cursor worth keeping."
        />
        <div class="container prose reveal max-w-[68ch]">
          <CodeBlock code={state} label="auth state" />
          <p><code>grr auth status</code> makes a live Gmail <code>users.getProfile</code> 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. <code>grr auth login</code> is interactive by default; <code>--device</code> is the only variant that works unattended, and it still needs a human to visit a URL and type a code.</p>
          <p>For incremental reads, keep the <code>history_id</code> that <code>auth status</code> returns and feed it to <code>grr gmail users history list --start-history-id</code> on the next run. That is the difference between fetching a bounded window and re-reading the mailbox.</p>
        </div>
      </div>
    </section>

    <section class="section" aria-labelledby="failure-title">
      <div class="container prose reveal max-w-[68ch]">
        <h2 id="failure-title">Failure modes worth handling explicitly</h2>
        <ul>
          <li><strong>Not authenticated.</strong> Exit non-zero, message names the rerun. Do not retry in a loop.</li>
          <li><strong>Expired without a refresh token.</strong> The error says to rerun <code>grr auth login</code>. This happens if a token was stored without a refresh token; it is not transient.</li>
          <li><strong>HTTP 403 naming a scope.</strong> The scope is outside what the build consented to. Not fixable at runtime &mdash; report it rather than retrying.</li>
          <li><strong>Unknown method id.</strong> grr returns ranked suggestions. Treat it as &ldquo;discover again&rdquo;, not as a crash: the index is refreshed daily and the surface moves.</li>
          <li><strong>Missing required parameter.</strong> The message names the parameter and lists the others. Re-read <code>grr api describe</code> rather than retrying variations.</li>
          <li><strong>Empty results.</strong> A valid empty array, not an error. <code>-f table</code> renders it as a <code>(no results)</code> header; the JSON form is just <code>[]</code>.</li>
          <li><strong>HTTP/2 fallback.</strong> <code>grr transport</code> showing <code>fell_back: true</code> means QUIC was blocked and HTTP/2 is in use. Nothing is wrong; do not retry or treat it as a fault.</li>
        </ul>
      </div>
    </section>

    <section class="section" aria-labelledby="mcp-title">
      <div class="container">
        <SectionIntro
          id="mcp-title"
          eyebrow="mcp"
          title={`Point the harness at \`grr mcp\` and it owns ${discoveryMethodTotal} tools.`}
          copy="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."
        />
        <div class="container prose reveal max-w-[68ch]">
          <p class="lead">Register <code>grr mcp</code> as a stdio server in any MCP client (Claude Desktop, Gemini CLI, VS Code, Cursor, OpenClaw) and <code>tools/list</code> returns every one of the {discoveryMethodTotal} methods with an inputSchema built from Discovery: typed properties, the required list, enum values, plus the <code>params</code>/<code>body_file</code>/<code>query</code>/<code>dry_run</code> escape hatches.</p>
          <ul>
            <li><strong>Tool name = the dotted method id</strong> (<code>gmail.users.messages.list</code>) &mdash; the same id <code>grr api call</code> takes, so a harness can switch between MCP and CLI without translating anything.</li>
            <li><strong><code>tools/call</code> shares the engine</strong> with the CLI: results come back as JSON text content; errors are <code>isError</code> results and never kill the server.</li>
            <li><strong><code>--readonly</code></strong> gives a server that refuses every write method with an actionable error result &mdash; the right default when an agent works on a user&rsquo;s behalf.</li>
          </ul>
        </div>
      </div>
    </section>

    <section class="section" aria-labelledby="ask-title">
      <div class="container">
        <SectionIntro
          id="ask-title"
          eyebrow="natural language"
          title="`grr ask` — plain English in, a typed plan out."
          copy={`A System One model (Jev by default, provider-agnostic by config) classifies the request hierarchically and fills the typed parameters, from the same ${discoveryMethodTotal}-method catalog.`}
        />
        <div class="container prose reveal max-w-[68ch]">
          <p class="lead"><code>grr ask "show my unread gmail"</code> classifies service ({discoveryServiceCount} options), then method, then one typed question per required parameter &mdash; every candidate is code-supplied from the discovery index, so the model can only <em>select</em>, never invent.</p>
          <ul>
            <li><strong>It prints the plan by default</strong> (method, params, url, scopes, confidence as JSON) and sends nothing to Google; the routing judgments themselves are calls to the System One provider. <code>--run</code> executes through the same engine as <code>grr api call</code>.</li>
            <li><strong>Confidence is visible</strong>: below the threshold (default 0.6) the plan is flagged, not hidden &mdash; a harness can decide whether to proceed or ask the user.</li>
            <li><strong>The safety gate applies here too</strong>: <code>--readonly</code>/<code>--deny-service</code> fire as soon as the method is resolved, before any judgment on parameters.</li>
          </ul>
          <p>Endpoint, model, API key and confidence threshold are the one tunable surface in grr; the <a href={`${base}docs/commands/#system-one`}>System One setup</a> on the Commands page covers the config file and the preferred <code>TYPESAFE_API_KEY</code> env route.</p>
        </div>
      </div>
    </section>
    <section class="section" aria-labelledby="harness-title">
      <div class="container prose reveal max-w-[68ch]">
        <h2 id="harness-title">Mapping this onto a harness</h2>
        <p>Claude Code, Codex, Cursor, Gemini CLI, OpenClaw, and anything else that shells out can be set up from the same sources, in this order:</p>
        <ul>
          <li><code>grr skills install</code> &mdash; writes the packaged core skill plus the 14 per-service skills into <code>~/.agents/skills/</code>, offline and globally (never a project directory); <code>--claude</code> mirrors into <code>~/.claude/skills/</code> for Claude Code, which reads nothing else. <code>grr skills list</code> reports the state of each target.</li>
          <li><a href="https://github.com/debanjanbasu/grr-cli/blob/main/skills/grr/SKILL.md" target="_blank" rel="noreferrer">skills/grr/SKILL.md ↗</a> &mdash; the packaged agent skill: the discovery-first discipline, the naming rule, and the output contract in one file. Install it with <code>npx skills add https://github.com/debanjanbasu/grr-cli</code>.</li>
          <li><a href={`${base}llms.txt`}>llms.txt</a> &mdash; the site&rsquo;s machine-readable index, for orientation. Fetch it once at session start.</li>
          <li><code>grr schema</code> &mdash; the generated command tree, for building tool definitions. Regenerate it rather than hand-writing flags.</li>
          <li><a href={`${base}#demo-title`}>The terminal demo</a> &mdash; {demoDurationSeconds} seconds of real grr output, including an opencode session driving grr the way a harness would.</li>
          <li><code>grr api list</code> and <code>grr api describe</code> &mdash; the method-level view of the same index, at runtime, with no network and no credential.</li>
        </ul>
        <p>
          The quickstart at the top of this page is the portable form of the same guidance &mdash; paste it into
          any harness&rsquo;s system prompt or bootstrap script, or install the skill and let the harness load it.
        </p>
      </div>
    </section>
  </DocsShell>
</BaseLayout>