Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Gmail, Calendar, Drive, Docs, Sheets via gws CLI or Python.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | 225% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 279% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 187% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 199% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 289% | 0% |
Gmail, Calendar, Drive, Contacts, Sheets, and Docs — through Hermes-managed OAuth and a thin CLI wrapper. When gws is installed, the skill uses it as the execution backend for broader Google Workspace coverage; otherwise it falls back to the bundled Python client implementation.
references/gmail-search-syntax.md — Gmail search operators (is:unread, from:, newer_than:, etc.)scripts/setup.py — OAuth2 setup (run once to authorize)scripts/google_api.py — compatibility wrapper CLI. It prefers gws for operations when available, while preserving Hermes' existing JSON output contract.The setup is fully non-interactive — you drive it step by step so it works on CLI, Telegram, Discord, or any platform.
Define a shorthand first:
bashGSETUP="python ${HERMES_HOME:-$HOME/.hermes}/skills/productivity/google-workspace/scripts/setup.py"
bash$GSETUP --check
If it prints AUTHENTICATED, skip to Usage — setup is already done.
Before starting OAuth setup, ask the user TWO questions:
Question 1: "What Google services do you need? Just email, or also Calendar/Drive/Sheets/Docs?"
himalaya skillinstead — it works with a Gmail App Password (Settings → Security → App Passwords) and takes 2 minutes to set up. No Google Cloud project needed. Load the himalaya skill and follow its setup instructions.
--services email,calendar during auth so the consent screen only asks for the scopes they actually need.
narrower --services set like calendar,drive,sheets,docs.
all service set.
Question 2: "Does your Google account use Advanced Protection (hardware security keys required to sign in)? If you're not sure, you probably don't — it's something you would have explicitly enrolled in."
allowed apps list before Step 4 will work. Let them know upfront.
Tell the user:
> You need a Google Cloud OAuth client. This is a one-time setup: > > 1. Create or select a project: > https://console.cloud.google.com/projectselector2/home/dashboard > 2. Enable the required APIs from the API Library: > https://console.cloud.google.com/apis/library > Enable: Gmail API, Google Calendar API, Google Drive API, > Google Sheets API, Google Docs API, People API > 3. Create the OAuth client here: > https://console.cloud.google.com/apis/credentials > Credentials → Create Credentials → OAuth 2.0 Client ID > 4. Application type: "Desktop app" → Create > 5. If the app is still in Testing, add the user's Google account as a test user here: > https://console.cloud.google.com/auth/audience > Audience → Test users → Add users > 6. Download the JSON file and tell me the file path > > Important Hermes CLI note: if the file path starts with /, do NOT send only the bare path as its own message in the CLI, because it can be mistaken for a slash command. Send it in a sentence instead, like: > The JSON file path is: /home/user/Downloads/client_secret_....json
Once they provide the path:
bash$GSETUP --client-secret /path/to/client_secret.json
If they paste the raw client ID / client secret values instead of a file path, write a valid Desktop OAuth JSON file for them yourself, save it somewhere explicit (for example ~/Downloads/hermes-google-client-secret.json), then run --client-secret against that file.
Use the service set chosen in Step 1. Examples:
bash$GSETUP --auth-url --services email,calendar --format json $GSETUP --auth-url --services calendar,drive,sheets,docs --format json $GSETUP --auth-url --services all --format json
This returns JSON with an auth_url field and also saves the exact URL to ~/.hermes/google_oauth_last_url.txt.
Agent rules for this step:
auth_url field and send that exact URL to the user as a single line.http://localhost:1 after approval, and that this is expected.Error 403: access_denied, send them directly to https://console.cloud.google.com/auth/audience to add themselves as a test user.The user will paste back either a URL like http://localhost:1/?code=4/0A...&scope=... or just the code string. Either works. The --auth-url step stores a temporary pending OAuth session locally so --auth-code can complete the PKCE exchange later, even on headless systems:
bash$GSETUP --auth-code "THE_URL_OR_CODE_THE_USER_PASTED" --format json
If --auth-code fails because the code expired, was already used, or came from an older browser tab, it now returns a fresh fresh_auth_url. In that case, immediately send the new URL to the user and have them retry with the newest browser redirect only.
bash$GSETUP --check
Should print AUTHENTICATED. Setup is complete — token refreshes automatically from now on.
~/.hermes/google_token.json and auto-refreshes.~/.hermes/google_oauth_pending.json until exchange completes.gws is installed, google_api.py points it at the same ~/.hermes/google_token.json credentials file. Users do not need to run a separate gws auth login flow.$GSETUP --revokeAll commands go through the API script. Set GAPI as a shorthand:
bashGAPI="python ${HERMES_HOME:-$HOME/.hermes}/skills/productivity/google-workspace/scripts/google_api.py"
bash# Search (returns JSON array with id, from, subject, date, snippet) $GAPI gmail search "is:unread" --max 10 $GAPI gmail search "from:boss@company.com newer_than:1d" $GAPI gmail search "has:attachment filename:pdf newer_than:7d" # Read full message (returns JSON with body text) $GAPI gmail get MESSAGE_ID # Send $GAPI gmail send --to user@example.com --subject "Hello" --body "Message text" $GAPI gmail send --to user@example.com --subject "Report" --body "<h1>Q4</h1><p>Details...</p>" --html $GAPI gmail send --to user@example.com --subject "Hello" --from '"Research Agent" <user@example.com>' --body "Message text" # Reply (automatically threads and sets In-Reply-To) $GAPI gmail reply MESSAGE_ID --body "Thanks, that works for me." $GAPI gmail reply MESSAGE_ID --from '"Support Bot" <user@example.com>' --body "Thanks" # Labels $GAPI gmail labels $GAPI gmail modify MESSAGE_ID --add-labels LABEL_ID $GAPI gmail modify MESSAGE_ID --remove-labels UNREAD
bash# List events (defaults to next 7 days) $GAPI calendar list $GAPI calendar list --start 2026-03-01T00:00:00Z --end 2026-03-07T23:59:59Z # Create event (ISO 8601 with timezone required) $GAPI calendar create --summary "Team Standup" --start 2026-03-01T10:00:00-06:00 --end 2026-03-01T10:30:00-06:00 $GAPI calendar create --summary "Lunch" --start 2026-03-01T12:00:00Z --end 2026-03-01T13:00:00Z --location "Cafe" $GAPI calendar create --summary "Review" --start 2026-03-01T14:00:00Z --end 2026-03-01T15:00:00Z --attendees "alice@co.com,bob@co.com" # Delete event $GAPI calendar delete EVENT_ID
bash# Search existing files $GAPI drive search "quarterly report" --max 10 $GAPI drive search "mimeType='application/pdf'" --raw-query --max 5 # Get metadata for a single file $GAPI drive get FILE_ID # Upload a local file (auto-detects MIME type) $GAPI drive upload /path/to/report.pdf $GAPI drive upload /path/to/image.png --name "Logo.png" --parent FOLDER_ID # Download (binary files download as-is; Google-native files export to a # sensible default — Docs→pdf, Sheets→csv, Slides→pdf, Drawings→png) $GAPI drive download FILE_ID $GAPI drive download DOC_ID --output ~/doc.pdf $GAPI drive download DOC_ID --export-mime text/plain --output ~/doc.txt # Create a folder $GAPI drive create-folder "Reports" $GAPI drive create-folder "Q4" --parent FOLDER_ID # Share $GAPI drive share FILE_ID --email alice@example.com --role reader $GAPI drive share FILE_ID --email alice@example.com --role writer --notify $GAPI drive share FILE_ID --type anyone --role reader # anyone with link $GAPI drive share FILE_ID --type domain --domain example.com --role reader # Delete — defaults to trash (reversible). Use --permanent to skip the trash. $GAPI drive delete FILE_ID $GAPI drive delete FILE_ID --permanent
bash$GAPI contacts list --max 20
bash# Create a new spreadsheet $GAPI sheets create --title "Q4 Budget" $GAPI sheets create --title "Inventory" --sheet-name "Stock" # Read $GAPI sheets get SHEET_ID "Sheet1!A1:D10" # Write $GAPI sheets update SHEET_ID "Sheet1!A1:B2" --values '[["Name","Score"],["Alice","95"]]' # Append rows $GAPI sheets append SHEET_ID "Sheet1!A:C" --values '[["new","row","data"]]'
bash# Read $GAPI docs get DOC_ID # Create a new Doc (optionally seeded with body text) $GAPI docs create --title "Meeting Notes" $GAPI docs create --title "Draft" --body "First paragraph..." # Append text to the end of an existing Doc $GAPI docs append DOC_ID --text "Additional content to append"
All commands return JSON. Parse with jq or read directly. Key fields:
[{id, threadId, from, to, subject, date, snippet, labels}]{id, threadId, from, to, subject, date, labels, body}{status: "sent", id, threadId}[{id, summary, start, end, location, description, htmlLink}]{status: "created", id, summary, htmlLink}[{id, name, mimeType, modifiedTime, webViewLink}]{id, name, mimeType, modifiedTime, size, webViewLink, parents, owners}{status: "uploaded", id, name, mimeType, webViewLink}{status: "downloaded", id, name, path, mimeType}{status: "created", id, name, webViewLink}{status: "shared", permissionId, fileId, role, type}{status: "trashed" | "deleted", fileId, permanent}[{name, emails: [...], phones: [...]}][[cell, cell, ...], ...]{status: "created", spreadsheetId, title, spreadsheetUrl}{status: "created", documentId, title, url}{status: "appended", documentId, inserted_at, characters}drive delete, prefer the default trash (reversible) over --permanent.setup.py --check. If it fails, guide the user through setup.skill_view("google-workspace", file_path="references/gmail-search-syntax.md").2026-03-01T10:00:00-06:00) or UTC (Z).| Problem | Fix | |---------|-----| | NOT_AUTHENTICATED | Run setup Steps 2-5 above | | REFRESH_FAILED | Token revoked or expired — redo Steps 3-5 | | HttpError 403: Insufficient Permission | Missing API scope — $GSETUP --revoke then redo Steps 3-5 | | AUTHENTICATED (partial) or "Token missing scopes" | New write capabilities (Drive write/delete, Docs create/edit) require re-authorization. $GSETUP --revoke then redo Steps 3-5 to grant the upgraded scopes. | | HttpError 403: Access Not Configured | API not enabled — user needs to enable it in Google Cloud Console | | ModuleNotFoundError | Run $GSETUP --install-deps | | Advanced Protection blocks auth | Workspace admin must allowlist the OAuth client ID |
bash$GSETUP --revoke
Other measured skills in the registry, with their headline benchmark lift.