AAC Google stack access (GCP project, service account, clasp token, Apps Script projects). Load before AAC Sheets, Drive, Gmail or Apps Script work, when a Google call fails on auth or scope, when clasp needs re-authenticating, and before claiming you lack Google access.
Install
npx skillscat add surreptakos/claude-dotfiles/aac-google-access Install via the SkillsCat registry.
AAC Google Cloud & Apps Script access
The access is on disk. Pick the transport by surface (From a cloud container below), and
verify a grant with tokeninfo before relying on any list here.
Identities
- One shared GCP project for all AAC Apps Script projects: ID
gpt-sheets-access-475817,
number594980791877. It owns the clasp OAuth client (594980791877-…) AND the service
account. Put"projectId": "gpt-sheets-access-475817"in each repo's.clasp.jsonsoclasp logsworks. - Service account (god-tier, no expiry):
gpt-sheets-access@gpt-sheets-access-475817.iam.gserviceaccount.com, key at~/.config/gpt-sheets-access-475817-853f8648243b.json. Use from Python
(from google.oauth2 import service_account→AuthorizedSession) for direct Sheets/Drive
REST: it bypasses the gen-AI-ineligible flag and never expires. That signing path is the PC
and Team transports only; a cloud container has no key on disk (see From a cloud
container). Share the target workbook Editor with the SA email to grant it access. The SA
cannot run Apps Script functions (scripts.run→ 404;executionApi.access: MYSELF). - clasp token (
~/.clasprc.json, userdjgatsakos@gmail.com): refresh atoauth2.googleapis.com/token. Verified by tokeninfo 2026-07-31, the grant is:mail.google.com(full Gmail, read AND send), full/auth/driveplusdrive.fileanddrive.metadata.readonly,spreadsheets,cloud-platform,service.management,logging.read,script.*(projects/deployments/scriptapp/external_request/container.ui/
webapp.deploy),userinfo.email,userinfo.profile,openid. An earlier note here claimeddrive.fileonly, which would have ruled out a working transport: verify with tokeninfo,
including against this list. The token now serves only Sheets/Drive REST and Gmail from a
machine that has it; never re-mint it for a deploy, and never tell the owner a deploy
needs it. - Auth model: bound Apps Script runs as the user (djgatsakos@gmail.com), never a service
account. So "no permission to call DriveApp.X / Required: /auth/drive" from a bound script means
the stored grant is behind the project's pinnedoauthScopes, not a token problem: re-authorize
once (interactive). Publishing the OAuth consent screen out of Testing is the only thing that
stops the 7-day token expiry.
Apps Script projects and scopes
- Canonical AAC scope block (superset of every AAC data/ops project's declared scopes; pin it
verbatim as eachappsscript.json'soauthScopesand authorize once, to stop scope-drift
breakage):spreadsheets,drive,mail.google.com,script.external_request,script.scriptapp,script.container.ui,userinfo.email. Scope enforcement is
lazy/per-call: a declared scope the code never calls is inert (no trigger breakage); the
grant only needs to catch up when code invokes that scope (or at the next interactive auth). - AAC ops projects (repo ↔ online name ↔ scriptId), all bound to
projectId gpt-sheets-access-475817:Sales Data KPIs/commissions↔ "AAC 2026 Sales Commissions
Script" (bound,1kt5rVEw…);Sales Data KPIs/aac-cockpit↔ "AAC Snapshot Pipeline"
(1Dd7GVub…);Financial/aac-bill-intake/gas↔ "AAC AP Intake (dry run)" (1hIdNvfr…). All
three carry the canonical block as of 2026-07-20. To enumerate the account's script projects:
Drivefiles.list mimeType='application/vnd.google-apps.script'+ Apps Scriptprojects.getContent; bound scripts like commissions are absent from that Drive list.
Deploying and running
- Deploys and headless runs go through
gas, no clasp token (2026-09-09). Every AAC Apps
Script repo deploys itself from GitHub (claude-dotfilesgas/; team skillgas-deploy): merge
to the default branch = release;node <claude-dotfiles>/gas/cli/gas.js run owner/repo <fn> '[args]' --wait 10=clasp run-function;gas pull <scriptId>=clasp pull;gas logs --project gpt-sheets-access-475817=clasp logs. - Other ways to run deployed code: (1)
clasp run-function <fn>: PUBLIC fns only (a
name ending in_is private), under the clasp token. (2)doPostweb app: runs as the owner with the full
grant (incl. Drive); gate it with a secret + whitelist,clasp deploya fresh version (the@HEADdeployment has no usable/exec), and POST via PowerShellInvoke-RestMethod
(curl 411s on Apps Script's 302). (3) The sheet menu or a trigger. - Execution history:
GET script.googleapis.com/v1/processes:listScriptProcesses?scriptId=…
(scopescript.processes); the service account can do this without clasp. Use exactly that
endpoint:processes?userProcessFilter.scriptId=…filters to processes the caller started, so
a service account gets0rows, which reads as "never ran" and is false.
Re-authenticating clasp
When the token dies (7-day cycle, consent screen in Testing), runnode "C:/Users/Dan/Claude/Projects/Meta/message-board/tools/clasp-auth.js" (or npm run auth
from that repo) and paste the command it prints. It works from any project: the credential is
machine-wide. The tool regenerates the full command from a required-scope list, finds the creds
file by matching client_id, and verifies the result with tokeninfo instead of trustingexpiry_date.
A bare clasp login is always wrong and fails silently, two ways (both verified against
installed clasp 3.3.0 source, 2026-08-01):
- Wrong OAuth client. clasp ships its own public client
1072944905499-…
(@google/clasp/build/src/auth/oauth_client.js); every AAC token belongs to the private client594980791877-1r31l7idb4nc5js9ag2d28s5eni5joj5, so--credspointing at that client's secret
JSON is mandatory. It lives at~/.config/client_secret_594980791877-….json, local and never
synced (moved out of OneDrive Downloads 2026-09-30 so a credential no longer syncs to every PC;
the tools'CREDS_SEARCH_DIRSinclude~/.config). - Scopes silently dropped. clasp's
DEFAULT_SCOPES(build/src/commands/login.js) omitspreadsheets, fulldrive,mail.google.comandscript.processes, andauthorize()never
sendsinclude_granted_scopes(build/src/auth/auth_code_flow.js), so anything not explicitly
requested is removed from the grant.~/.clasprc.jsonis shared by every AAC project, so
that quietly breaks Gmail/Drive work elsewhere whileclasp pushstill succeeds.
Done when tokeninfo shows the full grant AND the one CI copy of this credential is
refreshed: aac-cockpit's CLASPRC_JSON secret (used by deploy.yml + promote.yml; no other AAC
repo has one), via gh secret set CLASPRC_JSON < ~/.clasprc.json from the aac-cockpit repo.
Skip it and the next CI deploy dies on invalid_grant (it did on 2026-08-17).
From a cloud container
Which credential carries a Google call is a property of the surface, not of the caller. Three
transports reach the same identity, the service account above; share the workbook Editor with
that address and any of them can read it. One helper in claude-dotfiles picks between them:python3 <claude-dotfiles>/tools/google-rest.py transport|whoami|cell|get|doc-read|doc-replace.
Revising a Google Doc in place (issue 1216). The Drive connector's update_file changes only
title and folder; asked for new text it answers 400 Unknown name "textContent", and the agent
that then creates a "v2" leaves two documents for one piece of work. Revise the Doc over its own id:
- Share it Editor with the SA, through the connector, since a Doc the connector made is the
owner's:share_file(fileId, "gpt-sheets-access@gpt-sheets-access-475817.iam.gserviceaccount.com", "writer"). - Write the whole new body to a
.mdfile (or.html,.docx,.txt) and runpython3 <claude-dotfiles>/tools/google-rest.py doc-replace <docId> <file>. Drive converts it
into the Doc (files.update,uploadType=media), keeping the id, link, sharing and revision
history, and the command prints the body read back from the Doc.doc:<otherDocId>in place of
the file copies another Doc's body through.docxand exits 1 unless the two read-backs match. doc-read <docId>prints the current body as plain text; read it first, so the revision starts
from what is there.
Verified 2026-10-01 on the service-rates research Doc: doc-replace <v1 id> doc:<v2 id> read back
identical to v2, and v2 went to the trash through the connector's trash_file (only the owner can
trash a My Drive file, so the SA cannot).
- Pro/Max cloud session (
CLAUDE_CODE_REMOTE=true): the key is an environment API
credential held by the agent proxy, which mints the access token and attachesAuthorization
to every*.googleapis.comrequest. Send plaincurl/urllib/requestswith no
Authorization header and no key file tosheets.googleapis.com,www.googleapis.com/drive/v3,script.googleapis.com,cloudresourcemanager.googleapis.comandlogging.googleapis.com;
all arrive as the SA.google.oauth2.service_account/AuthorizedSessionare wrong here:
nothing is on disk to load, andgoogle-authis not installed. The proxy replaces a header
you send (a deliberately bogus bearer token still answers200as the SA), so code that signs
here fails silently, running as a principal it did not choose. - Team / Enterprise: those plans have no API-credentials section ("aren't available on Team
or Enterprise plans yet"), so the key JSON rides theGPT_SHEETS_SA_KEY_JSONenvironment
variable on an owner-only environment and the PC code path applies, reading the JSON from the
variable instead of the file. Check that variable beforeCLAUDE_CODE_REMOTE: a Team
container setsCLAUDE_CODE_REMOTE=truetoo, but its proxy attaches nothing, so the ambient
branch would send an unauthenticated request straight into a 401. - The PC: the key file and
service_account→AuthorizedSession, as under Identities.
Gmail rides none of them. mail.google.com on the SA token answers 400 Precondition check failed: the SA has no mailbox, and a personal @gmail.com cannot delegate domain-wide. Gmail from
a cloud session is the claude.ai Gmail connector; on the PC it is the clasp token. The helper
refuses Gmail URLs on every transport so the failure never reads as a scope problem.
Verify before believing any of the above (expected results from a Pro/Max cloud container,
2026-09-16, issue 172):
ls ~/.config/gpt-sheets-access-475817-853f8648243b.json # No such file or directory
echo "[$GPT_SHEETS_SA_KEY_JSON]" # []
python3 tools/google-rest.py transport # cloud-proxy CLAUDE_CODE_REMOTE (agent proxy attaches Authorization; we send none)
python3 tools/google-rest.py whoami # gpt-sheets-access@gpt-sheets-access-475817.iam.gserviceaccount.com
python3 tools/google-rest.py cell 1dBhSYwk1lHXAosMtbdQ4qI73Rb60BBiB4S6h42ruUQg "Script Errors!A1" # Timestamp
curl -sS -o /dev/null -w '%{http_code}\n' https://gmail.googleapis.com/gmail/v1/users/me/profile # 400whoami answering the SA address is the proof the transport is live; a 401 there means the
surface is not the one you assumed (a Team container with the variable unset, most often).