Google Workspace Skill for Hermes Agent: Gmail, Calendar, Drive, Sheets, Docs via CLI
Gmail, Calendar, Drive, Docs, Sheets via gws CLI or Python.
Written by Neura Market from the official Hermes Agent documentation for Google Workspace. Commands, paths, and version numbers are reproduced from the source unchanged.
Read the official documentationThe Google Workspace skill covers Gmail, Calendar, Drive, Contacts, Sheets, and Docs. Authentication uses Hermes-managed OAuth, and commands are dispatched through a lightweight CLI wrapper called gws. If gws is installed on the system, the skill delegates to it for broader Workspace coverage. Without gws, it falls back to the bundled Python client implementation, which covers the same core services but may lack some edge-case features or performance optimizations that the CLI provides. In practice, install gws if you need full API surface access or plan to script multi-step workflows outside the skill; the fallback is fine for standard read/write operations on the six listed services.
References
references/gmail-search-syntax.md, Documents Gmail search operators such asis:unread,from:, andnewer_than:. Use this reference when you need to filter or query Gmail messages programmatically or via the API. It pairs well with any automation that processes email, letting you narrow results to specific senders, timeframes, or read status. A common caveat: operators are case-insensitive but must be used exactly as documented; for example,newer_than:1dworks, butnewer_than:1 Dmay not.
Scripts
Two scripts live in the scripts/ directory. setup.py handles OAuth2 authorization; you run it once to grant permissions. google_api.py is a compatibility wrapper CLI. When the gws tool is available, it delegates operations to gws for better performance or feature coverage, but it always preserves the JSON output contract that Hermes already expects. This means you can swap between the two backends without changing downstream consumers.
First-Time Setup
The entire setup is non-interactive. You drive it step by step, so it works on CLI, Telegram, Discord, or any platform.
Define a shorthand first:
GSETUP="python ${HERMES_HOME:-$HOME/.hermes}/skills/productivity/google-workspace/scripts/setup.py"
Step 0: Check if already set up
$GSETUP --check
If it prints AUTHENTICATED, skip to Usage, setup is already done.
Step 1: Triage, ask the user what they need
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?"
- Email only → They don't need this skill at all. Use the
himalayaskill instead, 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. - Email + Calendar → Continue with this skill, but use
--services email,calendarduring auth so the consent screen only asks for the scopes they actually need. - Calendar/Drive/Sheets/Docs only → Continue with this skill and use a narrower
--servicesset likecalendar,drive,sheets,docs. - Full Workspace access → Continue with this skill and use the default
allservice 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."
- No / Not sure → Normal setup. Continue below.
- Yes → Their Workspace admin must add the OAuth client ID to the org's allowed apps list before Step 4 will work. Let them know upfront.
Step 2: Create OAuth credentials (one-time, ~5 minutes)
Tell the user:
You need a Google Cloud OAuth client. This is a one-time setup:
- Create or select a project: https://console.cloud.google.com/projectselector2/home/dashboard
- 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
- Create the OAuth client here: https://console.cloud.google.com/apis/credentials Credentials → Create Credentials → OAuth 2.0 Client ID
- Application type: "Desktop app" → Create
- 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
- 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:
$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.
Step 3: Get authorization URL
Use the service set chosen in Step 1. Examples:
$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:
- Extract the
auth_urlfield and send that exact URL to the user as a single line. - Tell the user that the browser will likely fail on
http://localhost:1after approval, and that this is expected. - Tell them to copy the ENTIRE redirected URL from the browser address bar.
- If the user gets
Error 403: access_denied, send them directly tohttps://console.cloud.google.com/auth/audienceto add themselves as a test user.
Step 4: Exchange the code
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:
$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.
Step 5: Verify
$GSETUP --check
Should print AUTHENTICATED. Setup is complete, token refreshes automatically from now on.
Notes
- Token is stored at
~/.hermes/google_token.jsonand auto-refreshes. - Pending OAuth session state/verifier are stored temporarily at
~/.hermes/google_oauth_pending.jsonuntil exchange completes. - If
gwsis installed,google_api.pypoints it at the same~/.hermes/google_token.jsoncredentials file. Users do not need to run a separategws auth loginflow. - To revoke:
$GSETUP --revoke
Usage
All commands are run through a single API script. Set GAPI as a shorthand to keep invocations short:
GAPI="python ${HERMES_HOME:-$HOME/.hermes}/skills/productivity/google-workspace/scripts/google_api.py"
Gmail
Search, read, send, reply, and manage labels. The search command returns a JSON array with id, from, subject, date, and snippet.
# 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
Calendar
List, create, and delete events. The list command defaults to the next 7 days. Create requires ISO 8601 timestamps with timezone.
# 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
Drive
Search, upload, download, create folders, share, and delete files. Upload auto-detects MIME type. Download exports Google-native files to a sensible default format (Docs to pdf, Sheets to csv, Slides to pdf, Drawings to png) unless you specify otherwise. Delete defaults to trash (reversible); use --permanent to skip the trash.
# 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
Contacts
$GAPI contacts list --max 20
Sheets
Create, read, write, and append rows to spreadsheets.
# 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"]]'
Docs
Read, create, and append text to documents.
# 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"
Output Format
Every command returns JSON. You can pipe the output into jq for filtering or read it directly in your automation script. The structure varies by endpoint, so here is what each operation returns:
- Gmail search: an array of objects, each containing
id,threadId,from,to,subject,date,snippet, andlabels. - Gmail get: a single object with
id,threadId,from,to,subject,date,labels, andbody. - Gmail send/reply: an object with
statusset to"sent", plusidandthreadId. - Calendar list: an array of objects, each with
id,summary,start,end,location,description, andhtmlLink. - Calendar create: an object with
statusset to"created", plusid,summary, andhtmlLink. - Drive search: an array of objects, each with
id,name,mimeType,modifiedTime, andwebViewLink. - Drive get: an object with
id,name,mimeType,modifiedTime,size,webViewLink,parents, andowners. - Drive upload: an object with
statusset to"uploaded", plusid,name,mimeType, andwebViewLink. - Drive download: an object with
statusset to"downloaded", plusid,name,path, andmimeType. - Drive create-folder: an object with
statusset to"created", plusid,name, andwebViewLink. - Drive share: an object with
statusset to"shared", pluspermissionId,fileId,role, andtype. - Drive delete: an object with
statusset to either"trashed"or"deleted", plusfileIdandpermanent. - Contacts list: an array of objects, each with
name,emails(an array of strings), andphones(an array of strings). - Sheets get: a 2D array of cell values, i.e.,
[[cell, cell, ...], ...]. - Sheets create: an object with
statusset to"created", plusspreadsheetId,title, andspreadsheetUrl. - Docs create: an object with
statusset to"created", plusdocumentId,title, andurl. - Docs append: an object with
statusset to"appended", plusdocumentId,inserted_at, andcharacters.
In practice, you will most often reach for this section when building a pipeline that chains multiple Google Workspace actions together. For example, after a Drive upload you might want to grab the returned id to share the file, or after a Calendar create you might use the htmlLink to notify a user. The status field is useful for error handling: check it before proceeding to the next step.
Rules
- Never send email, create/delete calendar events, delete Drive files, share files, or modify Docs/Sheets without confirming with the user first. Show what will be done (recipients, file IDs, content, share role) and ask for approval. For
drive delete, prefer the default trash (reversible) over--permanent. - Check auth before first use, run
setup.py --check. If it fails, guide the user through setup. - Use the Gmail search syntax reference for complex queries, load it with
skill_view("google-workspace", file_path="references/gmail-search-syntax.md"). - Calendar times must include timezone, always use ISO 8601 with offset (e.g.,
2026-03-01T10:00:00-06:00) or UTC (Z). - Respect rate limits, avoid rapid-fire sequential API calls. Batch reads when possible.
Troubleshooting
| 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 |
This table covers the most common issues you will hit when setting up or running the integration. The first four all point back to the same setup flow: if you see an auth-related error, the fastest path is usually to revoke and re-run the OAuth steps. The --revoke flag clears the stored token so you can start fresh. The partial auth message is especially easy to miss because the integration may appear to work for read-only operations before failing on writes. The Access Not Configured error means the Google Cloud Console project does not have the required API toggled on, which is a one-time project setup step, not something you fix in the integration itself. ModuleNotFoundError is a local dependency issue that the install-deps flag resolves. The Advanced Protection case is rare but important: if your Workspace uses Google's Advanced Protection Program, the OAuth client ID must be explicitly allowlisted by an admin, otherwise the auth request is silently blocked.
Revoking Access
Run the following command to revoke the OAuth tokens that the setup tool has stored for your Google Workspace account:
$GSETUP --revoke
You would reach for this when you want to disconnect Neura Market from your Google Workspace account entirely, for example before switching to a different Google account, after rotating credentials, or as part of a cleanup routine. The command removes the locally cached tokens, so the next time you run a command that needs Google Workspace access, the tool will prompt you to re-authenticate. Note that revoking tokens on the client side does not delete any data in Google Workspace itself; it only invalidates the current session from the tool's perspective.