Google Workspace Skill for Hermes Agent: Gmail, Calendar, Drive, Docs, Sheets
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 documentationIf you run Hermes Agent and need it to read your inbox, manage your calendar, or pull files from Drive, this bundled skill is the direct route. It wraps Gmail, Calendar, Drive, Contacts, Sheets, and Docs behind a single Python CLI, with OAuth handled once during setup. You would reach for it when you want an agent that can act on your Google data, not just answer questions about it.
What it does
The skill gives Hermes a command-line interface to the most common Google Workspace operations. You can search and read email, send messages and replies, list and create calendar events, find and download Drive files, upload new ones, share them, read and write spreadsheet cells, and create or append to documents. Every command returns JSON, so the agent can parse results and decide the next step without scraping text output.
The skill has two execution backends. If the gws CLI is installed on the system, the skill uses it for broader coverage. Otherwise it falls back to a bundled Python client. Either way, the output contract stays the same, so your agent code does not need to know which backend ran.
Before you start
This skill ships with Hermes Agent, so there is nothing to install separately. It runs on Linux, macOS, and Windows. The real prerequisite is a Google Cloud OAuth client. You need a project with the Gmail API, Google Calendar API, Google Drive API, Google Sheets API, Google Docs API, and People API enabled, plus an OAuth 2.0 Client ID of type "Desktop app". If the app is still in Testing, you must add your Google account as a test user.
The setup script is non-interactive, which means you drive it step by step and it works from the CLI, Telegram, Discord, or any other platform Hermes supports. You define a shorthand first so the commands are short enough to type repeatedly:
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: ~/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 go through the API script. Set GAPI as a shorthand:
GAPI="python ${HERMES_HOME:-$HOME/.hermes}/skills/productivity/google-workspace/scripts/google_api.py"
Gmail
# 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
The search command accepts the same operators you would type into Gmail's search box, so is:unread, from:, and newer_than: all work. The --max flag caps how many results come back, which keeps the JSON payload small. Reading a full message by ID gives you the body text, which is what you need for summarization or extraction. Sending supports plain text or HTML, and you can override the display name and address with --from. Replying threads automatically, so the conversation stays together in Gmail. Label operations let you mark messages read or move them into folders.
Calendar
# 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
Listing events without arguments shows the next week. You can narrow the window with explicit start and end times, but those must be ISO 8601 with a timezone offset or a trailing Z for UTC. Creating an event requires the same format; the command will not guess a timezone for you. Attendees are a comma-separated list in a single string. Deleting takes the event ID you get from a list or create call.
Drive
# 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
Drive search takes a plain text query by default, or a raw query when you pass --raw-query, which lets you filter by MIME type or other metadata. Uploads detect the file type automatically, but you can override the name and place the file in a specific folder. Downloads behave differently depending on the file: binary files come down as-is, while Google-native files export to a sensible format unless you override with --export-mime. Creating folders is straightforward, and sharing supports email addresses or link-based access with reader or writer roles. Deletion goes to the trash by default, which is reversible, and --permanent skips the trash entirely.
Contacts
$GAPI contacts list --max 20
This returns a list of contacts with names, email addresses, and phone numbers. It is useful when you need to resolve a name to an address before sending an email or adding an attendee.
Sheets
# 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"]]'
Sheets operations use A1 notation for ranges. Creating a spreadsheet optionally names the first sheet. Reading returns a nested array of cell values. Writing replaces the contents of the given range with the values you supply, and appending adds rows below the existing data. The --values argument is a JSON string, so you need to escape quotes carefully in a shell.
Docs
# 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"
Docs support reading the full content, creating a new document with an optional initial body, and appending text to the end of an existing one. This is enough for an agent to draft meeting notes, write a report, or add a section to a living document.
Output Format
All commands return JSON. Parse with jq or read directly. Key fields:
- Gmail search:
[{id, threadId, from, to, subject, date, snippet, labels}] - Gmail get:
{id, threadId, from, to, subject, date, labels, body} - Gmail send/reply:
{status: "sent", id, threadId} - Calendar list:
[{id, summary, start, end, location, description, htmlLink}] - Calendar create:
{status: "created", id, summary, htmlLink} - Drive search:
[{id, name, mimeType, modifiedTime, webViewLink}] - Drive get:
{id, name, mimeType, modifiedTime, size, webViewLink, parents, owners} - Drive upload:
{status: "uploaded", id, name, mimeType, webViewLink} - Drive download:
{status: "downloaded", id, name, path, mimeType} - Drive create-folder:
{status: "created", id, name, webViewLink} - Drive share:
{status: "shared", permissionId, fileId, role, type} - Drive delete:
{status: "trashed" | "deleted", fileId, permanent} - Contacts list:
[{name, emails: [...], phones: [...]}] - Sheets get:
[[cell, cell, ...], ...] - Sheets create:
{status: "created", spreadsheetId, title, spreadsheetUrl} - Docs create:
{status: "created", documentId, title, url} - Docs append:
{status: "appended", documentId, inserted_at, characters}
Because every command returns structured JSON, you can chain them in an agent loop: search for unread mail, extract a meeting time, create a calendar event, and send a confirmation, all without parsing prose.
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.
These rules exist because the skill can take irreversible actions. The confirmation requirement is not optional; an agent that sends mail or deletes files without asking is a liability. The timezone rule prevents the classic mistake of creating an event at the wrong hour. Rate limits matter because Google will throttle you if you fire off dozens of calls in a second.
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 |
Most failures trace back to one of three causes: the token is missing or stale, the API is not enabled, or the OAuth consent screen does not include the scopes you need. The table gives you the exact fix for each. When in doubt, revoke and redo the auth flow, it is the cleanest reset.
Revoking Access
$GSETUP --revoke
Run this when you want to disconnect the skill from your Google account entirely. It invalidates the stored token, and the next --check will report NOT_AUTHENTICATED.
When not to use it
The source is explicit about one case: if the user only needs email, skip this skill entirely. The himalaya skill works with a Gmail App Password, takes about two minutes to set up, and does not require a Google Cloud project. This skill's OAuth setup is worth the overhead only when you also need Calendar, Drive, Sheets, or Docs.
Limits and gotchas
- The OAuth flow requires a Google Cloud project with six APIs enabled. That is a real barrier for a casual user, and the setup steps are manual.
- Advanced Protection accounts will fail at Step 4 unless a Workspace admin allowlists the OAuth client ID. This is an organizational policy, not something you can fix from the skill.
- Calendar events must include a timezone. Omitting it will cause an error or, worse, create an event at the wrong time.
- Drive deletion defaults to trash, which is reversible, but
--permanentis not. Use it only after explicit user confirmation. - The token auto-refreshes, but if it is revoked or expires, you will see
REFRESH_FAILEDand need to redo Steps 3-5. - Newer write capabilities require re-authorization. If you see
AUTHENTICATED (partial), revoke and redo the flow to pick up the upgraded scopes.
What pairs with this
The related skill is himalaya, which covers email-only workflows with a much lighter setup. Use it when the user does not need the rest of Workspace. For anything beyond email, this skill is the one to load.