---
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 — 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> — 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’s parameters and scopes, <code>--dry-run</code> 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.</p>
<p><code>grr schema</code> 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 {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> → <code>grr gmail users messages list</code>), and parameter names become kebab-case flags (<code>userId</code> → <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’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 “unknown method” 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 — 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 “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 <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’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.</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 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."
/>
<div class="container prose reveal max-w-[68ch]">
<p class="lead">OAuth consent is granted at <code>grr auth login</code> for a small default set — Gmail, Drive, Calendar, Tasks, Docs, Sheets, Slides and the sign-in scopes — and widened with <code>grr auth login --service <name></code> when a task needs more; <code>--all-scopes</code> 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.</p>
<p>Because every command resolves its method against the index, scope reporting is the same on both routes: read <code>leastPrivilegeScope</code> from <code>grr api describe</code> to see what a call touches, though nothing gates on it.</p>
<p>That leaves one failure mode to expect, and it is worth not misdiagnosing:</p>
<ul>
<li>A <strong>403</strong> 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 <code>grr auth login</code> 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.</li>
</ul>
<p>The full breakdown — how the set is derived, and the trade-off it carries — 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 — Windows Credential Manager, macOS Keychain, or the Linux Secret Service over D-Bus — 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 “rerun <code>grr auth login</code>” 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 — report it rather than retrying.</li>
<li><strong>Unknown method id.</strong> grr returns ranked suggestions. Treat it as “discover again”, 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>) — 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 — the right default when an agent works on a user’s behalf.</li>
<li><strong>Protocol revision:</strong> the stateless <code>2026-07-28</code> one — call <code>tools/*</code> inline and <code>server/discover</code> reports <code>supportedVersions</code> plus the server identity, which rides in every result’s <code>_meta</code>. A minimal legacy <code>initialize</code> handshake is also answered, so standard MCP clients that still open with it connect instead of erroring.</li>
</ul>
<p>Google now ships its own <strong>remote MCP servers</strong> for Workspace (Gmail, Calendar, Drive, People, Chat on <code>*mcp.googleapis.com</code>, developer preview) plus 50+ <code>Google Cloud</code> ones. Those suit a harness that can hold a remote endpoint per service and a Cloud project with the APIs enabled and consent configured. <code>grr mcp</code> is the local alternative: one stdio process covering every API in the index, offline discovery, the CLI’s safety profiles, and nothing to deploy.</p>
</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 — 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 — 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> — writes the packaged core skill plus the {discoveryServiceCount} 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> — 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> — the site’s machine-readable index, for orientation. Fetch it once at session start.</li>
<li><code>grr schema</code> — 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> — {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> — 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 — paste it into
any harness’s system prompt or bootstrap script, or install the skill and let the harness load it.
</p>
</div>
</section>
</DocsShell>
</BaseLayout>