Gmail access here is OAuth against the user's own Google Cloud project. Nothing works until that project exists and consent has been granted once.
Run gmail doctor first, every time. It checks the whole chain (runtime, OAuth
client, token, scopes, egress, two live API calls) and every failing check names the exact
command or console page that fixes it. Do not guess from an error message when one command
gives you the diagnosis.
gmail doctor # full check, exits 2 if anything failed
gmail doctor --quick # local files only, no network
gmail doctor --json # the Envelope, for programmatic checks
Is it already set up?
gmail whoami # the account, live, or "not logged in"
First-time setup
The Google Cloud half is a browser task the user has to do themselves. Point them at the "Google Cloud setup" section of the project README rather than retyping it; it is roughly ten minutes and produces one file.
Two things that go wrong constantly and are worth stating up front:
- The OAuth client must be application type Desktop app. A Web client has no loopback
redirect and consent can never complete.
gmail doctorfails theoauth-clientcheck with this if they picked wrong. - The Cloud project must stay in Testing with the user added as a test user. Gmail's
gmail.modifyscope is "restricted", and a Published project with a restricted scope hard-blocks at consent with "Access blocked: has not completed the Google verification process" and no way to click through. The cost of Testing is that refresh tokens expire after 7 days, so roughly weekly they re-rungmail login --force. That is the correct trade for a personal tool.
Once ~/.config/gmail-skill/credentials.json exists:
gmail login # opens the browser, writes the token, prints the account
gmail doctor # confirm
Logging in
gmail login # no-op if a usable token is already there
gmail login --force # re-consent: after 7 days, or after a scope change
gmail login --read-only # gmail.readonly only; this account can never send
gmail login --profile work # a second account
gmail logout --profile work # forget it locally
gmail logout --revoke # and revoke the grant at Google
--force is the answer to almost every auth failure. Google issues a fresh refresh token
and a token carrying every scope.
Headless or over SSH
gmail login starts a loopback listener and opens a browser. On a box with no browser,
pin the port and forward it from the machine that has one:
ssh -L 8765:localhost:8765 user@host # on the laptop
gmail login --port 8765 --no-browser # on the host, then open the printed URL
Google's device-code flow is not an option: it does not accept Gmail scopes.
Several accounts
One OAuth client, one token file per profile. Every command takes --profile, and
$GMAIL_SKILL_PROFILE sets the default.
gmail login --profile work
gmail --profile work search "is:unread"
Reading a failure
| Code | What happened | What to do |
|---|---|---|
auth_required |
No token for this profile | gmail login |
auth_expired |
Refresh token dead: 7-day Testing expiry, password reset, or revoked | gmail login --force |
scope_insufficient |
Token predates a scope this verb needs | gmail login --force |
client_invalid |
No client JSON, or it is a Web client | README setup; the type must be Desktop app |
api_not_enabled |
Gmail API off for the Cloud project | Enable Gmail API in the console |
rate_limited |
Per-minute quota | Retriable. Wait, then batch fewer messages |
quota_exceeded |
Daily budget or the account send cap | Not retriable today |
Where things live
~/.config/gmail-skill/ (or $GMAIL_SKILL_HOME), directory 0700:
credentials.json is the OAuth client, <profile>.token.json is a live Gmail session at
0600. Never copy either into a repo, a chat, or a bug report. gmail doctor --json is
already redacted: it carries no token and no client secret.