Gws cli
Google Workspace CLI (gws) for Drive, Docs, Sheets, Slides, Gmail, Calendar, and Tasks. Use whenever the user mentions a Google Doc/Sheet/Slide/Drive file, a shared document, Gmail/inbox/email, a calendar event, or Workspace — including auditing subscriptions, recurring charges, or billing receipts, work vs personal accounts, and sweeping both inboxes. Do NOT use for Google Cloud Platform tasks (use gcloud) or non-Google services, or on claude.ai — it needs the local logged-in `gws` CLI.From its SKILL.md
npx -y skills add zachmayer/skills --skill gws-cliAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
10.7 KB, as published. Nobody here has run it
Google Workspace CLI reference. Source and docs: https://github.com/googleworkspace/cli
Commands follow gws <service> <resource> <method>. The CLI dynamically discovers all API methods via Google's Discovery Service — if a Workspace API supports it, gws can call it.
Safety
- Confirm with the user before write/delete commands. Sending email, creating events, and deleting files are not reversible.
- Prefer
--dry-runfor destructive operations to preview the request first.
Defaults
- Inbox =
is:inbox. When the user asks about their inbox, use--query 'is:inbox'. Usein:anywhereonly when explicitly searching beyond the inbox.
Helper Commands (Preferred)
Use helper commands (+verb) instead of raw API calls when available — they're simpler and handle encoding:
# Gmail
gws gmail +send --to [email protected] --subject 'Hello' --body 'Hi Alice!'
gws gmail +triage --max 10 --query 'is:inbox' # inbox
gws gmail +reply --id MSG_ID --body 'Thanks!'
gws gmail +reply-all --id MSG_ID --body 'Acknowledged.'
gws gmail +forward --id MSG_ID --to [email protected]
gws gmail +watch # stream new emails (NDJSON)
# Calendar — times MUST be full ISO 8601 with timezone offset
gws calendar +insert --summary 'Meeting' --attendee [email protected] \
--start '2026-03-12T10:00:00-05:00' --end '2026-03-12T10:30:00-05:00'
Raw API Commands
When helpers don't cover your use case, use the raw API:
Drive
gws drive files list --params '{"q": "name contains \"report\""}'
gws drive files list --params '{"q": "mimeType = \"application/vnd.google-apps.folder\""}' --page-all
gws drive files get --params '{"fileId": "FILE_ID"}'
gws drive files create --json '{"name": "test.txt", "mimeType": "text/plain"}' --upload ./test.txt
gws drive files export --params '{"fileId": "FILE_ID", "mimeType": "text/csv"}' > output.csv
Sheets
gws sheets spreadsheets values get --params '{"spreadsheetId": "SHEET_ID", "range": "Sheet1!A1:D10"}'
gws sheets spreadsheets values append --params '{"spreadsheetId": "SHEET_ID", "range": "Sheet1", "valueInputOption": "USER_ENTERED"}' --json '{"values": [["a", "b", "c"]]}'
gws sheets spreadsheets create --json '{"properties": {"title": "New Sheet"}}'
Gmail
gws gmail users messages list --params '{"userId": "me", "q": "is:unread after:2026/03/01"}'
gws gmail users messages get --params '{"userId": "me", "id": "MSG_ID"}'
Calendar
gws calendar events list --params '{"calendarId": "primary", "timeMin": "2026-03-11T00:00:00Z", "timeMax": "2026-03-12T00:00:00Z"}'
gws calendar freebusy query --json '{"timeMin": "...", "timeMax": "...", "items": [{"id": "[email protected]"}, {"id": "[email protected]"}]}'
Key Flags
| Flag | Description |
|---|---|
--params '{"key": "val"}' | Query and path parameters |
--json '{...}' | Request body |
--page-all | Auto-paginate (NDJSON output) |
--page-limit <N> | Max pages when using --page-all (default: 10) |
--page-delay <MS> | Delay between pages in ms (default: 100) |
--upload <file> | Multipart upload (Drive, Gmail attachments) |
-o, --output <PATH> | Save binary responses to file |
--dry-run | Preview request without sending |
--format json|yaml|table|csv | Output format (default: json) |
Schema Introspection
When unsure about parameters, inspect the schema:
gws schema drive.files.list
gws schema gmail.users.messages.send
gws gmail --help # list all gmail resources
gws drive files --help # list all file methods
Multiple Accounts
Supports multiple Google accounts (home, work, school, etc.) via ~/.claude/gws-accounts.json:
{
"default": {
"path": "~/.config/gws",
"email": "[email protected]",
"description": "Personal account"
},
"work": {
"path": "~/.config/gws/profiles/work",
"email": "[email protected]",
"description": "Work account"
}
}
Rules:
- Bare
gws ...uses the default account. No env var needed. - For non-default accounts, use the wrapper script:
~/.claude/skills/gws-cli/scripts/gws-as.sh <profile> <gws args...> - If the user names an account, email, or domain, use the matching profile.
- If ambiguous and the action is a write/send/delete, ask which account.
- If
gws-accounts.jsonis missing, assume only the default account exists.
# Default account
gws gmail +triage
# Non-default account
~/.claude/skills/gws-cli/scripts/gws-as.sh work gmail +triage
Auth
On a new machine, run gws auth setup first — it creates a GCP project, enables Workspace APIs, and generates an OAuth client. This is a one-time prerequisite before gws auth login will work. Requires gcloud CLI (brew install google-cloud-sdk).
gws auth setup # one-time per machine: GCP project + OAuth client
Canonical login (use this every time the token expires)
When gws returns a 401 / invalid_grant: Token has been expired or revoked (or a 403 with insufficient authentication scopes), run this exact command. It requests the full set of common Workspace scopes Zach uses (Drive, Docs, Sheets, Slides, Gmail, Calendar, Tasks) via explicit --scopes URLs so cloud-platform is excluded and the token isn't subject to RAPT re-auth:
gws auth login --scopes "\
https://www.googleapis.com/auth/drive,\
https://www.googleapis.com/auth/documents,\
https://www.googleapis.com/auth/spreadsheets,\
https://www.googleapis.com/auth/presentations,\
https://www.googleapis.com/auth/gmail.modify,\
https://www.googleapis.com/auth/calendar,\
https://www.googleapis.com/auth/tasks"
For the non-default profile, prefix with the config dir:
GOOGLE_WORKSPACE_CLI_CONFIG_DIR=~/.config/gws/profiles/work gws auth login --scopes "\
https://www.googleapis.com/auth/drive,\
https://www.googleapis.com/auth/documents,\
https://www.googleapis.com/auth/spreadsheets,\
https://www.googleapis.com/auth/presentations,\
https://www.googleapis.com/auth/gmail.modify,\
https://www.googleapis.com/auth/calendar,\
https://www.googleapis.com/auth/tasks"
Why this exact command
- Put each scope on its own line with
\continuations — don't rely on one long quoted line. OAuth scopes are whitespace-delimited on the wire, so any stray space or newline inside the scope string corrupts a scope. Double-quoting is necessary but NOT sufficient: it stops the shell from word-splitting, but a paste- or terminal-inserted line break that lands inside the quoted value survives as a literal newline and still breaks the request. (Real failure: a one-line paste wrapped inside thecalendarURL, so Google received two junk scopes —handttps://www.googleapis.com/auth/calendar— and rejected the login withError 400: invalid_scope.) In the multi-line form above, bash deletes each\+newline inside the double quotes and rebuilds the exact comma-joined string with zero embedded whitespace, and the short per-scope lines can't wrap mid-URL. If you ever collapse it back to one line, paste it into an editor first and confirm it is a single unbroken line before running it. --scopeswith explicit URLs, not-sshorthand. The-sflag silently injectscloud-platformscope into the token request, which triggers RAPT (Re-Authentication Proof Token) session control and forces re-auth every 1-24 hours. Explicit--scopesURLs do not getcloud-platformadded. The trade-off is that the URL form is verbose — accept it; the short-name form costs you a daily re-auth.- Full set, not minimal. Always request the full Drive/Docs/Sheets/Slides/Gmail/Calendar/Tasks bundle, even if the immediate task only needs one service. Workspace scopes are not subject to RAPT, so over-scoping has no token-lifetime cost, and it prevents the "I logged in for Gmail but Calendar still says insufficient scopes" loop.
gmail.modify, not full mail.gmail.modifycovers read/send/label/draft but cannot permanently delete. Usehttps://mail.google.com/only if you need to empty trash via API.- No
--fulland nocloud-platform. Both trigger RAPT. If you are being prompted to re-authenticate frequently (daily/weekly), the cause is one of those scopes.
After re-auth: clear the token cache
After gws auth login reports success, the encrypted credentials file (~/.config/gws/credentials.enc) is updated but the in-memory access-token cache (~/.config/gws/token_cache.json) can still hold the previous token without the new scopes. The next API call then returns 403 insufficient authentication scopes even though the new scopes were granted.
If that happens, force a refresh:
rm -f ~/.config/gws/token_cache.json
gws calendar calendars get --params '{"calendarId": "primary"}' # any call rebuilds the cache
If a future task needs a scope not in the list above (e.g. forms, keep, admin.directory), re-run the login command with that scope URL appended (and clear the cache after) — Google issues a new token covering the union.
Adding an account
mkdir -p ~/.config/gws/profiles/<name>
GOOGLE_WORKSPACE_CLI_CONFIG_DIR=~/.config/gws/profiles/<name> gws auth setup
GOOGLE_WORKSPACE_CLI_CONFIG_DIR=~/.config/gws/profiles/<name> gws auth login
Then add the profile to ~/.claude/gws-accounts.json. Each entry needs path (config dir) and description. email is optional but helps with disambiguation. If the file doesn't exist, create it:
{
"default": {
"path": "~/.config/gws",
"email": "[email protected]",
"description": "Personal account"
},
"<name>": {
"path": "~/.config/gws/profiles/<name>",
"email": "[email protected]",
"description": "Work account"
}
}
The default entry is the account used by bare gws commands. All other entries require ~/.claude/skills/gws-cli/scripts/gws-as.sh <name> to use.
Tips
- JSON output by default — pipe to
jqfor filtering --page-allstreams NDJSON; usejq -s '.'to collect into an array- Drive queries use Drive search syntax in the
qparam - Gmail queries use the same syntax as the Gmail search box
- File IDs are in Drive URLs:
https://docs.google.com/spreadsheets/d/SHEET_ID/edit