gmail-tools OAuth client secret rotation — local dev-machine sync
Trigger
Any gmail-tools script (gmail-query.js, send-digest.js, gmail-mutate.js, or any other script
using lib/credentials-path.js) fails immediately with:
ERROR: invalid_client
This includes failing on a freshnode auth.js browser consent flow — a brand-new authorization
code exchange still returns invalid_client. That rules out a stale/expired refresh token (which
would instead show invalid_grant) and points at the OAuth client (id/secret pair) itself being
rejected by Google.
How to confirm
Check Google Cloud Console → APIs & Services → Credentials for the OAuth client
(588063977139-d3o1pr8gulefpb3m4mimuu3k1lm7bao0.apps.googleusercontent.com as of 2026-08).
Look at the client secrets list — if the secret matching what’s on disk shows Wyłączono /
Disabled and a newer one shows Włączono / Enabled, the client was rotated and the local
files were never updated.
Confirm SOPS already has the current value (rotation usually updates SOPS as part of the
rotation flow, but does not propagate to this dev machine’s local files —
secrets-sync.yml only pushes to servers/Vercel, never to local dev credential files):
Confirm GMAIL_TOOLS_GCP_OAUTH_CLIENT_ID / GMAIL_TOOLS_GCP_OAUTH_CLIENT_SECRET are present.
Optionally cross-check the tail of google_tools_radieu_secret in a local .env.local fallback
against the SOPS value’s tail (last 3-4 chars only, never full value) to confirm which one is
current before trusting either.
Fix — resync local credential files from SOPS (values never printed)
Two files need patching, both on this Windows dev machine only:
Real results (not invalid_client) confirm the fix.
If SOPS does not yet have the current value (rotation happened only in GCP Console, not synced
to SOPS): pull the new secret from the Console’s download-JSON button, update
secrets/gmail-tools.env.sops via the standard SOPS edit playbook
(docs/playbooks/sops-edit-operations.md), canary-decrypt, commit/PR, then run the resync above.
Escalation
If node auth.js still fails with invalid_client after resyncing from a confirmed-current SOPS
value, the OAuth client itself may have been deleted (not just secret-rotated) — this needs a human
in Google Cloud Console to create a new Desktop-type OAuth client
(redirect URI http://localhost:4343/callback) and update SOPS with the new id+secret.
Prevention
secrets-sync.yml has no local-dev-machine sync target — by design, local files are documented as
“emergency fallback only; may be stale” ecosystem-wide. A GCP-console-triggered secret rotation
(outside the normal SOPS-edit-then-sync flow) will always desync local dev files silently until
something like this breaks. No automated fix planned; this playbook is the fix path. Consider adding
a canary check (node scripts/gmail-query.js --max 1) to session-start checks for any session that
plans to use gmail-tools, so this surfaces immediately rather than mid-task.