Claude Skill: gas-dev — Google Apps Script Development Assistant
Table of Contents
- Overview
- Framework Overview
- 1. Prerequisites
- 2. Project Structure
- 3. Development Workflow
- 4. Coding Preferences
- 5. Skill File Structure
- 6. Verification
Overview
A Claude skill that encodes the full Google Apps Script (GAS) development workflow — from project creation to deployment — so Claude can assist efficiently with any GAS project without re-learning the setup each time.
- Scope: Global (
~/.claude/skills/gas-dev/) - Trigger: Auto-triggered when working with Apps Script, clasp, Google Sheets apps, or web app deployment
- Location:
/Users/tommy/.claude/skills/gas-dev/
Framework Overview
┌─────────────────────────────────────────────────────────────────┐
│ GAS Development Framework │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ SETUP │───▶│ DEVELOP │───▶│ TEST │───▶│ DEPLOY │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ GCP Project Code.js Local Dev clasp push │
│ OAuth Creds index.html Mock Data clasp deploy │
│ clasp login styles.html http-server Same URL! │
│ accounts.json components Polyfill │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ INFRASTRUCTURE │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ accounts.json │ │
│ │ (Central Registry: projects, accounts, credentials) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ├── .accounts/<email>/ (per-account credentials) │
│ ├── tools/gws-auth/ (OAuth flow + API client) │
│ ├── tools/gws-client.js (Sheets/Drive API wrapper) │
│ └── tools/file-picker/ (Drive file whitelist UI) │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ARCHITECTURE (per project) │
│ │
│ ┌─ LOCAL ──────────────┐ ┌─ GOOGLE CLOUD ──────────────┐ │
│ │ │ │ │ │
│ │ ProjectName/ │ │ Google Drive/App/ │ │
│ │ ├── Code.js ─────┼──▶│ ├── ProjectName (GAS) │ │
│ │ ├── index.html │ │ ├── ProjectName (Sheet) │ │
│ │ ├── styles.html │ │ └── assets/ │ │
│ │ ├── components.html │ │ │ │
│ │ ├── server.js ───┐ │ │ Apps Script Runtime │ │
│ │ ├── mock-data.json│ │ │ ├── doGet() → HtmlService │ │
│ │ └── .clasp.json │ │ │ ├── Sheets API (database) │ │
│ │ │ │ │ ├── PropertiesService │ │
│ │ localhost:PORT ◀──┘ │ │ ├── MailApp (email) │ │
│ │ (dev server) │ │ └── DriveApp (files) │ │
│ └───────────────────────┘ └──────────────────────────────┘ │
│ │
│ clasp push ──────▶ sync code to GAS │
│ clasp deploy -i ─▶ update SAME web app URL │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ FRONTEND PATTERNS (iframe-compatible SPA) │
│ │
│ ┌─ Hash Router ───────────────────────────────────────────┐ │
│ │ #form ──▶ #success ──▶ #admin ──▶ #dashboard │ │
│ │ │ │ │ │ │
│ │ │ localStorage │ password │ Sheets data │ │
│ │ │ (persist state) │ check │ (cached) │ │
│ │ ▼ ▼ ▼ │ │
│ │ Form View Admin Login Dashboard View │ │
│ │ ├── Contact Info ├── Summary (charts) │ │
│ │ ├── Date Picker ├── Submissions │ │
│ │ ├── AI Questions └── Settings │ │
│ │ └── Submit → email │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Back button ✓ Refresh ✓ iframe ✓ Mobile ✓ │
│ │
└─────────────────────────────────────────────────────────────────┘
MULTI-ACCOUNT FLOW
┌──────────────┐ ┌──────────────┐
│ Account A │ │ Account B │
│ user@co.com │ │ user@gmail │
└──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
.accounts/user@co.com/ .accounts/user@gmail/
├── .clasprc.json ├── .clasprc.json
├── gws-credentials ├── gws-credentials
├── gws-token.json ├── gws-token.json
└── allowed-files.json └── allowed-files.json
│ │
└────────┬───────────────┘
▼
accounts.json
(central registry)
│
┌────────┴────────┐
▼ ▼
Project A Project B
(Account A) (Account B)
1. Prerequisites
1.1 CLI Tools
| Tool | Install | Purpose |
|---|---|---|
| Node.js (v18+) | brew install node |
Runtime for local dev servers and scripts |
| npm | Bundled with Node.js | Package management |
| npx | Bundled with npm | Run one-off packages (e.g. http-server) |
| clasp | npm install -g @google/clasp |
Google Apps Script CLI for push/deploy |
Note: gcloud CLI is NOT required. All GCP configuration is done via the web console.
1.2 Google Cloud Platform Setup (One-time, Manual)
All steps below are done in console.cloud.google.com — they cannot be automated via CLI.
Step 1: Create a GCP Project
- GCP Console → New Project → name it (e.g. “claude-integration”)
- Note the Project Number (used later to link Apps Script)
Step 2: Enable APIs
- GCP Console → APIs & Services → Library
- Search and enable each:
- Google Sheets API
- Google Drive API
- Apps Script API
- Gmail API (only if using MailApp with email aliases)
Step 3: Configure OAuth Consent Screen
- GCP Console → APIs & Services → OAuth consent screen
- Choose “External” (or “Internal” for Google Workspace orgs)
- Add required scopes:
spreadsheets,drive,script.projects - Add test users if in “Testing” mode
Step 4: Create OAuth Credentials
- GCP Console → APIs & Services → Credentials → Create Credentials → OAuth Client ID
| Type | Use | Save as |
|---|---|---|
| Desktop application | CLI/Node.js tools (clasp, gws-client) | gws-credentials.json |
| Web application | Browser-based file picker UI | gws-web-credentials.json |
- Download both JSON files and store in
.accounts/<email>/
Step 5: Link Apps Script to GCP (per project)
- Open Apps Script Editor for the project
- Project Settings → Google Cloud Platform (GCP) Project
- Enter the GCP Project Number
- This enables
clasp logsand advanced API access
1.3 Authentication & Multi-Account Management
Each Google account gets its own credential folder:
.accounts/
├── user1@gmail.com/
│ ├── .clasprc.json # clasp auth token
│ ├── gws-credentials.json # OAuth desktop credentials
│ ├── gws-web-credentials.json # OAuth web credentials
│ ├── gws-token.json # Access/refresh tokens (auto-generated)
│ └── allowed-files.json # Whitelisted Drive file IDs
├── user2@company.com/
│ └── ... (same structure)
Central registry: accounts.json at project root maps every project to its account, scriptId, sheetId, deploymentId, and webAppUrl.
Setup steps per account:
clasp login→ stores token in~/.clasprc.json- Copy to account folder:
cp ~/.clasprc.json .accounts/<email>/.clasprc.json - Run GWS auth:
cd tools/gws-auth && node auth.js <email>→ generatesgws-token.json - Run file picker:
cd tools/file-picker && npm start→ manageallowed-files.json
Switching accounts: cp .accounts/<email>/.clasprc.json ~/.clasprc.json
Multiple accounts can share the same GCP project for OAuth credentials.
2. Project Structure
2.1 Standard File Layout
Simple project (form, landing page):
ProjectName/
├── .clasp.json # scriptId, rootDir
├── .claspignore # exclude local dev files
├── appsscript.json # runtime config, webapp settings
├── Code.js # backend: doGet, CRUD, email
├── Index.html # main UI entry
└── styles.html # CSS (separate file)
Full-featured project (dashboard, SPA):
ProjectName/
├── .clasp.json
├── .claspignore
├── appsscript.json
├── Code.gs # backend
├── index.html # entry point with includes
├── styles.html # all CSS
├── header.html # nav/header component
├── forms.html # form components
├── dashboard.html # dashboard view
├── server.js # local dev server (NOT deployed)
├── package.json # local dev dependencies
├── mock-data.json # synced Sheet data for local testing
├── .settings.json # local settings cache
└── node_modules/ # NOT deployed
2.2 Mirrored Folder Structure (Local ↔ Google Drive)
Local project folders and Google Drive folders should mirror each other:
LOCAL: GOOGLE DRIVE:
~/Project/Appscript/ My Drive/App/
├── ProjectA/ ├── ProjectA/
│ ├── .clasp.json │ ├── ProjectA (Apps Script)
│ ├── Code.js │ ├── ProjectA (Google Sheet)
│ ├── Index.html │ └── (other assets)
│ └── ... │
├── ProjectB/ ├── ProjectB/
│ └── ... │ └── ...
├── .accounts/ │
├── accounts.json │
└── tools/ │
- Each project has a local folder AND a Drive folder with the same name
- The Drive folder contains: the Apps Script file, the Google Sheet, and any uploaded assets
- When creating a new project, create both the local folder and Drive folder
- Store the Drive folder ID as
driveFolderIdin accounts.json
2.3 .claspignore (always include)
node_modules/**
server.js
package.json
package-lock.json
mock-data.json
.settings.json
.clasp.json
.claspignore
2.3 accounts.json Structure
{
"projects": {
"ProjectName": {
"account": "user@domain.com",
"scriptId": "...",
"sheetId": "...",
"deploymentId": "...",
"webAppUrl": "https://script.google.com/macros/s/.../exec",
"webAppAccess": "Anyone_Anonymous",
"driveFolderId": "...",
"note": "Deploy: clasp push && clasp deploy -i <deploymentId>"
}
},
"accounts": {
"user@domain.com": {
"clasp": ".accounts/user@domain.com/.clasprc.json",
"gws": ".accounts/user@domain.com/gws-credentials.json",
"gcpProject": "project-name"
}
},
"default": "user@domain.com"
}
Note: The accounts.json format may evolve as more projects are added. The skill should always read the current accounts.json to understand available projects rather than hardcoding assumptions about its structure.
2.4 Real Project Examples
Google Drive root folder: App
| Project | Account | GAS Type | Web App Access | Drive Folder |
|---|---|---|---|---|
| PayU | hkmci.com | Standalone | Anyone | App/PayU/ |
| Master Draw | hkmci.com | Sheet-bound | Anyone | App/Master Draw/ |
| Master Form | hkmci.com | Standalone | Anyone_Anonymous | App/Master Form/ |
Deployment & Embed Links:
| Project | Apps Script Web App | iframe Embed |
|---|---|---|
| PayU | Deploy Link | mcai.dev/payu |
| Master Draw | Deploy Link | masterconcept.ai/link/master-draw |
| Master Form | Deploy Link | masterconcept.ai/link/eohk |
2.5 Standalone vs Sheet-Bound Apps Script
There are two types of Apps Script projects:
Standalone GAS (e.g. PayU, Master Form)
- Created via
clasp create— produces an independent.gsfile in Drive - Can be moved freely between Drive folders via API
- Has its own scriptId, separate from any Sheet
- Appears as a separate file in Drive folder alongside the Sheet
Sheet-Bound GAS (e.g. Master Draw)
- Created from within a Google Sheet (Extensions → Apps Script)
- The script is embedded inside the Sheet — no separate file in Drive
- Cannot be moved independently via Drive API (move the Sheet instead)
parentsreturnsundefinedwhen querying via Drive API- Access via: open Sheet → Extensions → Apps Script
- The scriptId still works with clasp for push/deploy
How to tell which type:
- Query
drive.files.get(scriptId, { fields: 'parents' }) - If
parentsisundefined→ likely sheet-bound - If
parentshas a folder ID → standalone
When to use which:
- Standalone — when the web app is the primary product, Sheet is just the database
- Sheet-bound — when the Sheet is the primary product, script adds functionality to it
3. Development Workflow
3.1 Creating a New Project
- Create Google Sheet via Sheets API (using
gws-client.js) - Create Drive folder for the project
clasp create --title "ProjectName"in project folder (creates standalone GAS)- Move GAS file to Drive folder via Drive API
- Set up
appsscript.jsonwith timezone, webapp config - Register in
accounts.jsonwith all IDs - Create boilerplate: Code.js, Index.html, styles.html, .claspignore
- First push + deploy:
clasp push && clasp deploy - Save deploymentId to accounts.json
3.2 Local Development
For simple projects: Use npx http-server -p 3850 -c-1 to serve files locally with no caching.
For projects needing API access: Create server.js with:
- Express server on custom port
<?!= include('filename') ?>tag processing (reads local HTML files)google.script.runpolyfill using Proxy pattern → routes to/api/<functionName>endpoints- API endpoints that mirror Code.js functions using
gws-client.js - Template expression replacement (e.g.
<?= getAppIcon() ?>)
Mock data pattern:
tools/sync-mock-data.js— pulls live Sheet data intomock-data.json- Local server reads
mock-data.jsonfor instant responses - Run
node tools/sync-mock-data.jsto refresh local data
3.3 Deployment
CRITICAL RULE: Always deploy to the same URL.
# Switch account if needed
cp .accounts/<email>/.clasprc.json ~/.clasprc.json
# Push code then deploy to existing deployment
clasp push --force
clasp deploy -i "<deploymentId>" -d "Description"
- NEVER use
clasp deploywithout-iflag (creates new deployment = new URL) - ALWAYS use
clasp deploy -i <deploymentId>to update existing deployment - Store deploymentId in
accounts.jsonfor reference
4. Coding Preferences
4.1 File Organization
- Break down large HTML into modular includes:
styles.html,header.html, component files - Use
<?!= include('filename') ?>for server-side HTML includes - Keep each file focused — refactor when a file grows beyond manageable size
- CSS in separate
styles.html, not inline in the main page - One Code.js/Code.gs for backend (unless very large, then split by domain)
4.2 Backend Patterns (Code.js)
// Cached SpreadsheetApp
var _ss = null;
function SS() { if (!_ss) _ss = SpreadsheetApp.openById(SHEET_ID); return _ss; }
// doGet with page routing
function doGet(e) {
var page = e && e.parameter && e.parameter.page;
return HtmlService.createHtmlOutputFromFile('Index')
.setTitle('App Title')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
// Include helper
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
// Settings via PropertiesService
function getSetting(key) {
return PropertiesService.getScriptProperties().getProperty(key) || '';
}
// Date formatting (HKT)
var now = Utilities.formatDate(new Date(), 'Asia/Hong_Kong', 'yyyy-MM-dd HH:mm:ss');
4.3 Key Gotchas
| Issue | Cause | Fix |
|---|---|---|
google.script.run returns null |
Can’t serialize Date objects or complex objects in ANYONE_ANONYMOUS mode | Convert Dates to ISO strings; return simple strings (‘ok’) not objects |
alert() blocked |
iframe sandbox blocks modals | Use inline error messages instead |
Phone +852 shows #ERROR in Sheet |
+ interpreted as formula |
Strip non-digits in frontend, or prefix with ' in backend |
| Form hung on “Submitting…” | Return value can’t serialize | Return 'ok' string, not { success: true } |
| Timestamp wrong timezone | new Date() returns UTC in anonymous mode |
Use Utilities.formatDate(new Date(), 'Asia/Hong_Kong', ...) |
4.4 Frontend Patterns
Hash-based SPA routing (for iframe compatibility):
- Routes:
#form,#success,#admin,#dashboard hashchangeevent listener for navigationlocalStorageto persist current view across refresh- Success page: use
justSubmittedflag — don’t persist on refresh navigate()function handles same-hash re-routing
iframe embedding & branding:
Apps Script web app URLs are long and ugly (e.g. script.google.com/macros/s/AKfyc.../exec). The standard practice is to embed them in an iframe on a branded domain (e.g. masterconcept.ai/link/eohk or mcai.dev/payu). This has important implications:
- Web app access MUST be
ANYONEorANYONE_ANONYMOUS— if the GAS requires Google login (MYSELForDOMAIN), the OAuth prompt cannot render inside an iframe and the app will fail to load - For private apps, use password protection instead of Google auth — implement a client-side password check (hash-based) or server-side via Script Properties, rather than relying on Google login
setXFrameOptionsMode(ALLOWALL)is required in doGet for iframe embeddingalert()/confirm()/prompt()are blocked in iframe sandbox — always use inline error messages and custom UI insteadlocalStorageworks within the iframe origin for state persistence across refresh- Hash routing creates browser history entries so back button works inside the iframe
iframe-specific gotchas:
google.script.runsuccess/failure callbacks may silently fail if the return value can’t be serialized — always return simple strings- Authorization popups from
google.script.runcannot display inside iframe — all server functions must be pre-authorized by running them once in the script editor - Form submissions that hang with no response usually mean the function needs re-authorization
Responsive design:
- Raleway font + Material Icons Outlined
- Mobile breakpoints: 768px (tablet), 540px (mobile), 360px (small mobile)
inputmode="numeric"for phone fields (shows number keyboard on mobile)- Font size 16px on mobile inputs (prevents iOS zoom)
5. Skill File Structure
~/.claude/skills/gas-dev/
├── SKILL.md # Main skill (auto-triggered)
└── references/
├── project-setup.md # New project creation checklist
├── local-dev.md # Local server, polyfill, mock data
├── deployment.md # Clasp push/deploy, same-URL rule
├── frontend-patterns.md # SPA routing, iframe, localStorage, responsive
├── backend-patterns.md # Code.js patterns, Sheets CRUD, email, gotchas
├── gws-client.md # Google Workspace API usage from Node.js
└── accounts.md # Multi-account setup, accounts.json, credentials
SKILL.md Frontmatter
---
name: gas-dev
description: >
Google Apps Script development assistant. Handles project creation, local dev servers,
clasp deployment, Google Sheets as database, Script Properties for settings, and
web app frontend with iframe/SPA support. TRIGGER when: working in an Appscript
project directory, user mentions Apps Script / GAS / clasp / doGet / google.script.run,
creates Google Sheets apps, deploys web apps, builds forms or dashboards with
Google Sheets backend, or asks about Google Workspace API integration.
Even if user just says "create a form" or "deploy it" in an Appscript context,
use this skill.
---
SKILL.md Body Sections
- Quick Reference — most common commands (push, deploy, switch account, sync data)
- Project Structure — standard layout, accounts.json, .claspignore
- Account Management — switch accounts, credential paths
- Creating a New Project — step-by-step with API calls
- Local Development — server setup, mock data, polyfill
- Deployment Rules — ALWAYS same URL, push before deploy
- Frontend Patterns — routing, iframe, localStorage, responsive
- Backend Patterns — doGet, Sheets CRUD, email, settings
- Coding Preferences — modular HTML includes, separate styles, keep files short
- Common Gotchas — Date serialization, alert() blocked, return strings not objects
- Reference pointers — when to read each reference file
6. Verification
- Check skill appears in Claude Code’s available skills list (look for
gas-devin system reminder) - Test auto-triggering with prompts: “create a new Apps Script project”, “deploy my web app”, “add a Google Sheet as database”
- Verify reference files load when relevant (check if Claude reads them during complex tasks)
- End-to-end test: create project → local dev → push → deploy → verify same URL