---
name: adom-google
description: "Programmatic read+write access to a user's WHOLE Google Workspace — Gmail, Contacts, Drive, Sheets, Docs, Slides, Calendar, Tasks, Google Chat — plus a generic `api` passthrough to hit ANY Google API. CLI + OAuth only (no browser automation). Public/open-source; used daily by the Adom team. Auth is the account-specific part — Adom employees use it out of the box; other orgs either self-host their own OAuth gateway or email support@adom.inc for a managed OAuth container (~$5/mo). First run just authorizes with FULL Workspace access (no mode question to ask the user). Use when setting up/installing adom-google, authorizing Google, reading Gmail attachments/.ics, creating/listing/searching Contacts, editing Sheets/Docs/Slides, Drive files, Calendar events, posting to Google Chat, or any Google Workspace automation. For a brand-new ORG that needs its own Google OAuth client (non-Adom team getting started), use the companion `adom-google-onboarding` skill — it drives the user's real browser through Google Cloud Console and auto-configures everything. Trigger words — adom-google, install adom-google, set up adom-google, configure adom-google, google workspace cli, edit google sheet/doc/slides, drive upload, calendar event, post to google chat, post to chat as me, message colby, send a chat, dm <person>, gmail attachment, google contacts, people api, full workspace access, google oauth gateway broker, google api passthrough, self-host oauth, managed oauth container, onboard my org to google, set up google workspace for my company/team, configure google oauth for <org>, get my company on adom-google, create our google cloud oauth client."
---

# adom-google

One CLI for a user's Google Workspace from inside an Adom container. **REST-direct** —
it calls the Google APIs straight, owns auth via the **Adom OAuth Gateway**, and stores a
long-lived refresh token `0600`. No browser automation, no per-user Google Cloud project.

```
adom OAuth layer   one shared Adom OAuth client + Gateway redirect → refresh token (0600)
adom-google        modular wrapper: gmail · contacts · (drive/calendar = +1 scope line)
Google REST APIs   called directly (gmail.googleapis.com, people.googleapis.com, …)
```

## Why REST-direct (not a base-CLI shim)

We evaluated wrapping a base CLI — Google's `googleworkspace/cli` (`gws`, Rust,
Discovery-driven, JSON, agent skills), `gog`/`gogcli` (Go), GAM/GAMADV-XTD3, the
Gemini CLI, and `gcloud`. Conclusion: **call the REST APIs directly.**

- **Auth is the part Adom must own anyway.** One shared internal OAuth client + the
  Gateway's static redirect + a long-lived refresh token is Adom-specific; no base CLI
  speaks it. Adopting one means reverse-engineering its keyring/credential format to
  inject our token — more work than one `requests.post` to the token endpoint.
- **The data surface we need is tiny and stable.** gmail.readonly attachment reads + a
  handful of People API calls. Each is one HTTPS request. A base CLI adds a process
  boundary, an install/version dependency, and output coupling for no benefit at this size.
- **No per-user GCP project.** REST-direct with the shared Adom client satisfies the hard
  constraint. `gws`/`gog`/GAM all push users toward their own OAuth client or `gcloud`.
- **Gemini CLI is the wrong layer** (an AI coding agent, not a Workspace data client) and
  **gcloud can't touch Workspace user data** (GCP-only). `gws` is the closest "official"
  Google Workspace CLI and is a reasonable **future accelerator** if we ever need broad
  Sheets/Docs/Chat coverage — but for Gmail+Contacts it's overkill.

The code is structured so a `gws` backend *could* be slotted behind the same subcommands
later; today the win is zero new runtime dependencies and a clean migration from adom-gmail.

## Onboarding a brand-new org (give a team its OWN Google client)

If a **non-Adom** org wants to get going on their own, they need their own Google OAuth client.
Don't make them slog through Google Cloud Console by hand — use the **`adom-google-onboarding`**
skill, which drives the user's real logged-in browser (adom-desktop `nbrowser_*`) through the
console, auto-fills the fields (especially the redirect URI), captures the new client, and wires
it in RELAY mode (their secret stays `0600` on their box; Adom only lends a shared callback URL).
Kick it off with `adom-google onboard --org <slug>` (or `--json` for the exact values). Adom
employees skip all this — they install `adom-google-adom` and just run `auth`.

## Install

```bash
adom-wiki pkg install adom/adom-google     # copies the CLI to ~/.local/bin + skills to ~/.claude/skills
```

`install.sh` **copies** real files (no symlinks): the binaries land in `~/.local/bin` and each
skill in `~/.claude/skills/<name>/`. Runtime: Python 3 + `requests` (already in the container).
The Gateway WebSocket handshake is implemented in the stdlib (no `ws`/`websockets` package).

## First-run setup — just run `adom-google auth` (no mode quiz)

When setting up adom-google for the first time (it's unconfigured — `adom-google setup` or any
command prints a `Hint:` block), **do NOT ask the user any scope/mode question.** Full Workspace
access is the default and the tool "just works." Simply run:

```bash
adom-google auth
```

This grants full Workspace scopes (`gmail.modify, contacts, drive, spreadsheets, documents,
presentations, calendar, tasks, chat.messages, chat.spaces.readonly`) plus the generic
`adom-google api <url>` passthrough for **any** Google API. It prints a Google consent URL; the
user clicks **Allow** once.

> ⚠ **Open the consent URL as ONE value — never unquoted in a shell.** It contains `&`; if you
> drop it into an unquoted shell/open command the shell truncates it at the first `&`, Google
> receives only `client_id`, and shows *"Error 400: invalid_request — missing response_type"*
> (this bit real users). Safe: hand the URL to a browser tool with the URL **inside JSON**
> (`adom-desktop nbrowser_open_window '{"sessionId":"gauth","url":"<URL>"}'`), or single-quote the
> whole URL. The `adom-google auth` output prints the ready-to-paste safe command for you.

> ⏱ **The user has ~10 minutes to click Allow.** The gateway keeps the sign-in session alive
> with a WebSocket heartbeat, so there's no rush. If they ever see a "Session Expired" page, just
> re-run `adom-google auth`.

*(Advanced/rare: `adom-google auth --safe` grants narrower create-only scopes — only the
Drive/Sheets/Docs/Slides files this app makes, no "delete all". Do NOT prompt for it; use it
only if the user explicitly asks to minimize scopes.)*

## Auth — one consent, long-lived refresh token

```bash
adom-google auth            # authorize (FULL Workspace access — the default; ~10 min to approve)
adom-google auth --safe     # advanced: narrower create-only scopes (no delete-all)
adom-google auth --manual   # fallback: desktop-client paste flow
adom-google auth-code '<code>'   # finish --manual
adom-google status          # shows access + what's authorised (never prints secrets)
```

### How Adom handles the OAuth (Gateway broker flow)

Google requires one fixed `redirect_uri`, but every container has a unique hostname. The
Adom OAuth Gateway gives one static callback for all containers **and** acts as a
confidential-client **token broker**, so the client secret never reaches user containers:

1. `adom-google auth` builds the Google consent URL with
   `redirect_uri = <your gateway>/callback` (from the resolved provider) and a random `state`,
   and opens a WebSocket to the Gateway registering `{state, app:"adom-google", exchange:true}`.
2. The user approves in their browser (their own Google Workspace account).
3. Google redirects to the Gateway `/callback`; the Gateway **does the code→token exchange
   itself** (using the secret it alone holds) and pushes `{type:"tokens", …}` over the WS.
4. The container stores only the **refresh token** `0600` — no client secret. Later refreshes
   go through the gateway's `POST /refresh`, so the secret stays on the gateway forever.

**The package ships NO provider** — no gateway URL, no client_id, nothing Adom-specific. The
provider (`{gateway_url, client_id, app}`) is resolved at runtime, later wins: bundled
`provider.json` → legacy `credentials.json` → `~/.config/adom-google/provider.json` → env
(`OAUTH_GATEWAY_URL`, `ADOM_GOOGLE_CLIENT_ID/SECRET/APP`). With none configured, every command
prints the NO_PROVIDER hint. Adom employees install the private **`adom-google-adom`** package,
which writes the Adom provider (gateway + Internal client_id) to `~/.config/adom-google/provider.json`;
self-hosters run `adom-google provider set --gateway <url> --client-id <id>`. The client secret is
never in any package — in broker mode it lives only on the gateway's `creds/<app>.json` (0600).

If the Gateway is down, `auth --manual` uses a desktop-client loopback paste flow (that one
exchanges locally and does need a client secret via `init`).

### Getting auth as a NON-Adom user (this tool is public)

The code is open source; the OAuth app + gateway are account-specific. If a user is **not**
on Adom's Workspace, the bundled Internal client won't let them sign in — Google blocks
non-adom.inc accounts at consent. Two options to surface to them:

1. **Managed by Adom (easiest):** email **support@adom.inc** — Adom stands up a dedicated
   OAuth gateway container for their org (~$5/mo) wired to a Google client for their domain;
   they then get the full adom-google capability set, same as the Adom team.
2. **Self-host (DIY):** fork the repo; create their own Google Cloud project + OAuth client
   (Internal to their Workspace), enable the APIs they need, run the open-source OAuth gateway
   (`john/service-oauth`) with their client secret in `creds/<app>.json`, then point the CLI at
   theirs: `adom-google provider set --gateway https://their-gateway --client-id <their-id>`
   (writes `~/.config/adom-google/provider.json`). Google **Chat** also needs a one-time
   Chat-app configuration (name/avatar/description) in the Cloud console's Chat API →
   Configuration tab, or `spaces.*` calls 404 with "Chat app not found".

## Commands

```bash
# Generic passthrough — ANY Google API, signed with the user's token. This is the workhorse:
# Sheets/Docs/Slides/Drive/Calendar/Chat/Tasks/YouTube all go through here, zero per-API code.
adom-google api <url> [-X METHOD] [-d '<json>'|@file] [-q key=val ...] [--raw]
#   e.g. create a sheet:   adom-google api -X POST https://sheets.googleapis.com/v4/spreadsheets -d '{"properties":{"title":"BOM"}}'
#        post to Chat:      adom-google api -X POST https://chat.googleapis.com/v1/spaces/<id>/messages -d '{"text":"shipped"}'
#        calendar event:    adom-google api -X POST https://www.googleapis.com/calendar/v3/calendars/primary/events?sendUpdates=all -d '{...}'

# Gmail (read; behaviour-compatible with old adom-gmail)
adom-google gmail attachments <messageId>
adom-google gmail read <messageId> <attachmentId> [-o file]
adom-google gmail ics  <messageId>

# Contacts (People API; read + write)
adom-google contacts create --name "First Last" [--org O] [--title T] \
    [--email a@b.com ...] [--phone +1... ...] [--notes "..."] [--json]
adom-google contacts list [--limit N] [--json]
adom-google contacts search <query> [-v] [--json]

# Google Chat — posts as the SIGNED-IN USER (their own identity, NO attribution prefix).
# This is distinct from `adom-gchat`, which posts via an org webhook with "(on behalf of …)".
adom-google chat spaces [--json]                          # list your spaces (id · type · name)
adom-google chat dm <email> [--json]                      # resolve the 1:1 DM space with a person
adom-google chat send --to <space-id|email> "<text>"      # post; email auto-resolves the DM
#   "<text>" may be '-' (stdin) or '@file'; add --thread <key> to reply in a thread
adom-google chat read --from <space-id|email> [--limit N] [--json]   # recent messages

adom-google status     # access (full/safe) + what's authorized (never prints secrets)
adom-google accounts   # list all configured accounts (work + personal, isolated)
```

## Multiple accounts (v0.8.0+) — e.g. work + personal, fully isolated

`adom-google --account <name> <cmd>` (or `ADOM_GOOGLE_ACCOUNT=<name>`) runs ANY command against a
named account stored at `~/.config/adom-google/accounts/<name>/` with its OWN refresh token, mode,
and provider.json (inherits the default account's provider unless overridden — a Workspace-Internal
OAuth client can't sign in a gmail.com user, so a personal account typically needs its own External
client via `--account <name> provider set`). The default (unnamed) account keeps the original paths —
existing setups are untouched. First-run auth for a new account follows the SAME ask-the-user
each account authorizes independently: `adom-google --account personal auth`.

**`chat` vs [adom-gchat](/adom/adom-gchat):** adom-google posts as **you** and does the
**full Chat lifecycle** — post, read, edit, delete, react — as your own Google identity
(clean, no prefix). For an **attributed bot post** to a team space (deploys/alerts as
"Kel (on behalf of john)"), use **[adom-gchat](/adom/adom-gchat)**, the post-only webhook bot.
Rule of thumb: posting *as yourself* ⇒ `adom-google chat`; an *attributed bot announcement* ⇒
`adom-gchat`. The `chat.messages` + `chat.spaces.readonly` scopes are in both safe and full
mode, so this works out of the box once authorized.

We use Google Contacts to store **service identities** (portal/account metadata for an API
vendor), e.g. the JPMorgan Payments developer portal. Keep secrets (client secrets, portal
passwords) in a password manager — **never** in a contact's notes.

## Adding Drive / Calendar

Add the scope to `SCOPES` at the top of `bin/adom-google`, re-run `adom-google auth`, and
add a few REST calls against `www.googleapis.com/drive/v3` or `/calendar/v3`. Token refresh
and the `_request()` helper are already generic.

## Security

- Config `~/.config/adom-google/config.json` is created `0600`; the client secret and
  refresh token are never echoed to stdout/stderr or logs (`status` prints presence only).
- The Gateway only ever sees the short-lived auth `code`, never tokens or the client secret.

## Migration from adom-gmail

See [MIGRATION.md](MIGRATION.md). Short version: `adom-gmail` is now a forwarding shim;
its old `~/.config/adom-gmail/config.json` is auto-imported on first `adom-google` run
(gmail.readonly only — run `adom-google auth` once to add the `contacts` scope).
