website/docs/user-guide/messaging/google_chat.md
Connect Hermes Agent to Google Chat as a bot. The integration uses Cloud Pub/Sub pull subscriptions for inbound events and the Chat REST API for outbound messages. Equivalent ergonomics to Slack Socket Mode or Telegram long-polling: your Hermes process does not need a public URL, a tunnel, or a TLS certificate. It connects, authenticates, and listens on a subscription — the same way a Telegram bot listens on a token.
Run
hermes gateway setupand pick Google Chat for a guided walk-through.
:::note Workspace edition
Google Chat is part of Google Workspace. You can use this integration with a
personal Workspace (@yourdomain.com registered through Google) or a work
Workspace where you have the Admin rights to publish an app. Gmail-only accounts
cannot host Chat apps.
:::
| Component | Value |
|---|---|
| Libraries | google-cloud-pubsub, google-api-python-client, google-auth |
| Inbound transport | Cloud Pub/Sub pull subscription (no public endpoint) |
| Outbound transport | Chat REST API (chat.googleapis.com) |
| Authentication | Service Account JSON with roles/pubsub.subscriber on the subscription |
| User identification | Chat resource names (users/{id}) + email |
You need a Google Cloud project to host the Pub/Sub topic. If you don't have one, create it at console.cloud.google.com — personal accounts get a free tier that easily covers bot traffic.
Note the project ID (e.g., my-chat-bot-123). You'll use it in every subsequent
step.
In the console, go to APIs & Services → Library and enable:
Both are free for the volumes a personal bot generates.
IAM & Admin → Service Accounts → Create Service Account.
hermes-chat-botAfter creation, open the SA, go to Keys → Add Key → Create new key → JSON and
download the file. Save it somewhere only Hermes can read (e.g.,
~/.hermes/google-chat-sa.json, chmod 600).
:::caution There is NO "Chat Bot Caller" role A common mistake is to search for a Chat-specific IAM role and grant it at the project level. That role doesn't exist. Chat bot authority comes from being installed in a space, not from IAM. All your SA needs is Pub/Sub subscriber on the subscription you create in the next step. :::
Pub/Sub → Topics → Create topic.
hermes-chat-eventsAfter creation, the topic's detail page has a Subscriptions tab. Create one:
hermes-chat-events-subOn the topic (not the subscription), add an IAM principal:
[email protected]Pub/Sub PublisherWithout this, Google Chat cannot publish events to your topic and your bot will never receive anything.
On the subscription, add your own Service Account as a principal:
hermes-chat-bot@<your-project>.iam.gserviceaccount.comPub/Sub SubscriberAlso grant Pub/Sub Viewer on the same subscription — Hermes calls
subscription.get() at startup as a reachability check.
Go to APIs & Services → Google Chat API → Configuration.
projects/<your-project>/topics/hermes-chat-events.Save.
Open Google Chat in a browser. Start a DM with your app by searching for its name
in the + New Chat menu. The first time you message it, Google sends an
ADDED_TO_SPACE event that Hermes uses to cache the bot's own users/{id} for
self-message filtering.
Add the Google Chat section to ~/.hermes/.env:
# Required
GOOGLE_CHAT_PROJECT_ID=my-chat-bot-123
GOOGLE_CHAT_SUBSCRIPTION_NAME=projects/my-chat-bot-123/subscriptions/hermes-chat-events-sub
GOOGLE_CHAT_SERVICE_ACCOUNT_JSON=/home/you/.hermes/google-chat-sa.json
# Authorization — paste the emails of people allowed to talk to the bot
[email protected],[email protected]
# Optional
GOOGLE_CHAT_HOME_CHANNEL=spaces/AAAA... # default delivery destination for cron jobs
GOOGLE_CHAT_MAX_MESSAGES=1 # Pub/Sub FlowControl; 1 serializes commands per session
GOOGLE_CHAT_MAX_BYTES=16777216 # 16 MiB — cap on in-flight message bytes
The project ID also falls back to GOOGLE_CLOUD_PROJECT, and the SA path falls
back to GOOGLE_APPLICATION_CREDENTIALS — use whichever convention you prefer.
Install the dependencies the Google Chat adapter needs (no Hermes extra is currently published — install them directly):
pip install google-cloud-pubsub google-api-python-client google-auth google-auth-oauthlib
Start the gateway:
hermes gateway
You should see a log line like:
[GoogleChat] Connected; project=my-chat-bot-123, subscription=<redacted>,
bot_user_id=users/XXXX, flow_control(msgs=1, bytes=16777216)
Send "hola" in the test DM. The bot posts a "Hermes is thinking…" marker, then edits that same message in place with the real response — no "message deleted" tombstones.
Google Chat renders a limited markdown subset:
| Supported | Not supported |
|---|---|
*bold*, _italic_, ~strike~, `code` | Headings, lists |
| Inline images via URL | Interactive Card v2 buttons (v1 of this gateway) |
Native file attachments (after /setup-files — see Step 10) | Native voice notes / circular video notes |
The agent's system prompt includes a Google Chat–specific hint so it knows these limits and avoids formatting that won't render.
Message size limit: 4000 characters per message. Longer agent responses are automatically split across multiple messages.
Thread support: when a user replies inside a thread, Hermes detects the
thread.name and posts its reply in the same thread, so each thread gets a
separate Hermes session.
Out of the box the bot can post text, inline images via URL, and download cards for audio/video/documents. To deliver native Chat attachments — the same file widget you get when a human drags-and-drops a file — each user authorizes the bot once via a per-user OAuth flow.
Google Chat's media.upload endpoint hard-rejects service-account auth:
This method doesn't support app authentication with a service account. Authenticate with a user account.
There's no IAM role or scope that fixes this. The endpoint only accepts user credentials. So the bot has to act as a user whenever it uploads a file — specifically, as the user who asked for the file.
# Default profile:
python -m plugins.platforms.google_chat.oauth \
--client-secret /path/to/client_secret.json
# A named profile gets its own separate registration:
hermes -p <profile> python -m plugins.platforms.google_chat.oauth \
--client-secret /path/to/client_secret.json
That writes the client secret into the active profile's Hermes home (e.g.
~/.hermes/google_chat_user_client_secret.json for the default profile). The
client secret is profile-scoped, not shared across profiles — each profile
registers its own. This is deliberate: profiles are isolated auth boundaries, so
two profiles can point at different Google OAuth apps / accounts. Register it
once per profile that needs Google Chat attachment delivery.
Each user runs the flow once, in their own DM with the bot:
/setup-files to the bot. It replies with status and the next
step./setup-files start. The bot replies with an OAuth URL.http://localhost:1/?...&code=.... That failure is expected — the auth
code is in the URL bar.code=... value) and paste it back
into chat as /setup-files <PASTED_URL>. The bot exchanges it for a
refresh token.The token lands at ~/.hermes/google_chat_user_tokens/<sanitized_email>.json.
Subsequent file requests in that user's DM use their token, so the bot
uploads as them and the message lands in their space.
To revoke later: /setup-files revoke deletes only that user's token. Other
users' tokens are untouched.
The flow requests exactly one scope: chat.messages.create. That covers both
media.upload and the messages.create that references the uploaded
attachmentDataRef. No Drive, no broader Chat scopes — this is least-privilege
on purpose.
When the asker has no per-user token yet, the bot falls back to a legacy
single-user token at ~/.hermes/google_chat_user_token.json (if present from
a pre-multi-user install). When neither is available, the bot posts a clear
text notice telling the asker to run /setup-files.
A user revoking only clears their own slot. A 401/403 from one user's token evicts only that user's cache. Users don't disrupt each other.
Bot stays silent after sending "hola."
GOOGLE_CHAT_SERVICE_ACCOUNT_JSON
and that the SA is listed as Pub/Sub Subscriber on the subscription.[email protected] must have Pub/Sub Publisher.hermes gateway logs for [GoogleChat] Connected. If you see
[GoogleChat] Config validation failed, the error message tells you which
env var to fix.Bot replies but an error message appears instead of the agent's answer.
Check logs for [GoogleChat] Pub/Sub stream died — if these repeat, your SA
credentials may have been rotated or the subscription deleted. After 10 attempts
the adapter marks itself fatal.
"403 Forbidden" on every outbound message.
The bot was removed from the space, or you revoked it in the Chat API console.
Re-install it in the space (the next ADDED_TO_SPACE event will re-enable
messaging automatically).
Too many "Rate limit hit" warnings.
The Chat API's default quotas allow 60 messages per space per minute. If your agent produces long streaming responses that exceed that, the adapter retries with exponential backoff — but you'll still see user-visible latency. Consider concise responses or raising the quota in the GCP console.
Bot keeps posting the "/setup-files" notice instead of files.
The asker has no per-user OAuth token and there's no legacy fallback. Run
/setup-files in their DM and follow Step 10. After the exchange completes
the next file request uploads natively without a gateway restart.
/setup-files start says "No client credentials stored."
The one-time setup wasn't done for this profile (the client secret is profile-scoped, so a registration under one profile won't be seen by another). From a terminal, run it under the profile the gateway uses:
# Default profile:
python -m plugins.platforms.google_chat.oauth \
--client-secret /path/to/client_secret.json
# Named profile:
hermes -p <profile> python -m plugins.platforms.google_chat.oauth \
--client-secret /path/to/client_secret.json
Then send /setup-files start again.
/setup-files <PASTED_URL> says "Token exchange failed."
The auth code is single-use and short-lived (typically a few minutes). Send
/setup-files start to get a fresh URL and retry.
chat.bot and pubsub scopes.
IAM should be the actual enforcement — grant your SA the minimum
(roles/pubsub.subscriber + roles/pubsub.viewer on the subscription), not
project-level or org-level Pub/Sub roles.googleapis.com, drive.google.com, lh[3-6].googleusercontent.com, and
a few others). Any other host is rejected before the HTTP request, to
protect against SSRF scenarios where a crafted event could redirect the
bearer token to the GCE metadata service.agent/redact.py. The debug envelope dump
(GOOGLE_CHAT_DEBUG_RAW=1) routes through the same redaction filter and
logs at DEBUG level.chat.messages.create — the minimum that covers media.upload plus the
follow-up messages.create. Tokens are persisted as plain JSON at
~/.hermes/google_chat_user_tokens/<sanitized_email>.json (filesystem
permissions are the protection — same model as the SA key file). Each
token is owned by exactly one user; revoke is scoped to that user.