← Back to the collection

GWS (Google Workspace CLI)

A Claude Code plugin for interacting with Google Workspace via the official gws CLI — starting with uploading markdown files to Google Drive as formatted Google Docs.

Skills

md-to-google-doc

Upload a local markdown file to Google Drive as a Google Doc, or update an existing one. Handles:

Usage

# Create a new Google Doc in a Drive folder
/md-to-google-doc ./notes.md FOLDER_ID

# With a custom title
/md-to-google-doc ./notes.md FOLDER_ID --title "My Document"

# Update an existing Google Doc
/md-to-google-doc ./notes.md "https://docs.google.com/document/d/DOC_ID/edit"

The folder ID is the last segment of a Drive folder URL: https://drive.google.com/drive/u/0/folders/FOLDER_ID_HERE

google-doc-to-md

Download a Google Doc as a local markdown file using the Drive API’s native text/markdown export.

/google-doc-to-md DOC_ID_OR_URL [output.md] [--title]

gws-account

Manage multiple Google accounts for the gws CLI. Each account lives in its own config directory under ~/.config/gws-accounts/<label>/.

/gws-account add <label>      # Add account (browser OAuth)
/gws-account list             # List accounts
/gws-account switch <label>   # Switch active account
/gws-account current          # Show active account

gws-calendar

Query and manage Google Calendar events for the active account: list events, look up a single meeting, extract Meet/Zoom links, list calendars, create events.

# List today's events
bash plugins/gws/scripts/calendar-list-events.sh

# Tomorrow's events
bash plugins/gws/scripts/calendar-list-events.sh --from=tomorrow --to=tomorrow

# Just the conference links for today
bash plugins/gws/scripts/calendar-links.sh

# Find a specific event
bash plugins/gws/scripts/calendar-get-event.sh --match "coaching"

# List accessible calendars (★ = primary)
bash plugins/gws/scripts/calendar-list-calendars.sh

# Check your access to one calendar before writing to it
bash plugins/gws/scripts/calendar-list-calendars.sh --id "<calendarId>"

# Create event with auto-generated Meet link
bash plugins/gws/scripts/calendar-create-event.sh \
  --title "Sync" --start "2026-05-08T14:00" --end "2026-05-08T15:00" \
  --attendees "a@x.com,b@y.com" --meet

# All-day event — a date-only --start is all it takes
bash plugins/gws/scripts/calendar-create-event.sh --title "Offsite" --start 2026-09-28

# Multi-day all-day event, inclusive last day
bash plugins/gws/scripts/calendar-create-event.sh \
  --title "Conference" --start 2026-09-28 --through 2026-09-30
Date specs: today tomorrow yesterday YYYY-MM-DD +Nd +Nw. All scripts accept --json for programmatic output.

All-day events use Google’s {"date": …} bounds rather than {"dateTime": …}; calendar-create-event.sh picks the right shape from whether --start carries a time, and refuses mixed bounds up front instead of letting the API answer with an opaque 400 Bad Request. Google’s end date is exclusive — --through takes the inclusive last day so you don’t have to remember that.

gmail-read

Search Gmail and read messages for the active account. Runs a Gmail search query and returns structured results (id, subject, from, date, snippet), and optionally full message bodies with HTML stripped to plain text.

bash plugins/gws/skills/gmail-read/scripts/read.sh "from:boss@example.com newer_than:7d"

gmail-draft-from-markdown

Convert a local markdown file into a Gmail draft (never sends). Markdown is converted to HTML and saved as a draft; you review and send from Gmail’s UI. Supports local file attachments and threading the draft onto an existing message as a reply, and reads the draft back to confirm the DRAFT label and the attachment filenames before reporting success. Large drafts are uploaded as message/rfc822 rather than passed inline, so neither big attachments nor a long body hits the kernel’s argv limit.

bash plugins/gws/skills/gmail-draft-from-markdown/scripts/draft.sh ./reply.md \
  someone@example.com --subject "Follow-up"

# Threaded reply with attachments
bash plugins/gws/skills/gmail-draft-from-markdown/scripts/draft.sh ./reply.md \
  someone@example.com --reply-to 19f3de2c4f9ee065 \
  --attach ~/Documents/proposal.pdf --attach ~/Documents/site-plan.pdf

youtube

Manage YouTube playlists for the active account: list playlists, list items, add/remove items, cleanup. See skills/youtube/SKILL.md for the full command set. YouTube uses its own OAuth login (youtube-login.sh) separate from the core Google Workspace scopes.

Prerequisites

Check auth status with scripts/auth-preflight.sh, not a hand-rolled gws auth status check:

bash plugins/gws/scripts/auth-preflight.sh          # human-readable
bash plugins/gws/scripts/auth-preflight.sh --json   # machine-readable
bash plugins/gws/scripts/auth-preflight.sh --quiet  # guard: silent, exit code only

Two reasons it exists. First, gws writes Using keyring backend: keyring to stderr on every invocation, so anything shaped like gws auth status 2>&1 | python3 -c '...json.load...' hits a parse error every time and reports a false “NOT AUTHENTICATED”. Read 2>/dev/null, never 2>&1. Second, it separates three states a bare exit-code check conflates: authenticated, no account selected (fix with /gws-account switch <label> — re-running OAuth is a pointless detour), and genuinely no credentials (gws auth login). It also warns when calls are silently going to the default account because nothing is selected — the usual cause of work landing on a personal calendar.

Raw gws calls: --params is the URL, --json is the body

The scripts in this plugin already get this right; the trap is for ad-hoc calls. On a write (create/insert/update/patch), body fields passed via --params are silently discarded — exit 0, a well-formed resource comes back, and the fields you sent are simply gone:

# WRONG → creates an "Untitled" octet-stream file in My Drive root
gws drive files create --params '{"name":"Notes","mimeType":"application/vnd.google-apps.folder","parents":["FOLDER_ID"]}'

# RIGHT
gws drive files create --json '{"name":"Notes","mimeType":"application/vnd.google-apps.folder","parents":["FOLDER_ID"]}'

Reads take --params (fileId, q, fields genuinely are query params), which is why the habit forms. --dry-run prints the body / query_params split and is the cheapest pre-flight for any write. gws schema drive.files.create (dotted path, not subcommands) says which keys are query params. Full write-up in skills/account/SKILL.md.

Accounts & Authentication

gws authenticates with a Google OAuth “installed app” client (client_secret.json) that you supply — created in a Google Cloud project that has the Workspace APIs you need enabled (Drive, Docs, Sheets, Gmail, Calendar, Slides, Tasks). The same client can authenticate any account allowed by its OAuth consent screen.

Single account (default). Put the client at ~/.config/gws/client_secret.json, then gws auth login and pick the account in the browser. All state (encrypted credentials, token cache) lives in ~/.config/gws/.

Multiple accounts. The gws-account skill manages additional accounts, each in its own config directory under ~/.config/gws-accounts/<label>/ with its own client_secret.json, credentials, and token. Switching writes the chosen label to ~/.config/gws-accounts/.active, which every account-aware script respects across shell sessions and agent invocations.

/gws-account add work        # browser OAuth into a new labeled account
/gws-account switch work     # make 'work' the active account
/gws-account switch default  # back to ~/.config/gws
/gws-account current         # show the active account

Different accounts can use different OAuth clients. gws-account add copies the default client into the new account dir; if a given account needs a different client (e.g. an org-internal OAuth app for a managed Workspace domain), place that account’s client_secret.json into its ~/.config/gws-accounts/<label>/ dir before logging in, rather than using add (which would overwrite it with the default client). Then log in with GOOGLE_WORKSPACE_CLI_CONFIG_DIR=~/.config/gws-accounts/<label> gws auth login.

Re-authenticating. If a token expires (invalid_grant / invalid_rapt / “Authentication expired”), re-run gws auth login for the affected account (prefix with GOOGLE_WORKSPACE_CLI_CONFIG_DIR=… for a labeled account).

Install

claude plugins add /path/to/claude-plugins/plugins/gws

What’s Included

File Purpose
skills/md-to-google-doc/SKILL.md Skill definition — triggers, usage, troubleshooting
scripts/gdoc.sh Router: auto-detects create vs update
scripts/upload.sh Creates a new Google Doc from markdown
scripts/update.sh Updates an existing Google Doc from markdown
scripts/clean.sh Strips YAML frontmatter and Obsidian callouts
references/MANUAL.md Step-by-step fallback if scripts are unavailable
skills/google-doc-to-md/SKILL.md Download a Google Doc as markdown
skills/account/SKILL.md Multi-account management for the gws CLI
skills/calendar/SKILL.md Calendar events, meet/zoom links, calendar listing
skills/gmail-read/SKILL.md Search Gmail and read messages (headers, snippet, plain-text body)
skills/gmail-draft-from-markdown/SKILL.md Create Gmail drafts from markdown (never sends)
skills/youtube/SKILL.md YouTube playlist management (list, add/remove, cleanup)
scripts/calendar-list-events.sh List events in a date range
scripts/calendar-get-event.sh Get a single event by id or fuzzy match
scripts/calendar-links.sh Extract Meet/Zoom links for events
scripts/calendar-list-calendars.sh List accessible calendars, or check access to one (--id)
scripts/calendar-create-event.sh Create a timed or all-day calendar event (optional Meet link)
scripts/auth-preflight.sh Report the precise auth/account state; guard for other scripts

Disclaimer

This plugin wraps the third-party open-source gws CLI (Apache-2.0). Per its own README:

⚠️ This is not an officially supported Google product.

It’s provided as-is, with no warranty.