Autonomous Gmail triage as an in-process Hermes plugin
Project description
hermes-inbox-organizer
Autonomous Gmail triage as an in-process Hermes plugin — it sorts, labels, and archives your mail and drafts replies in your voice, self-hosted, with no third-party SaaS in the path of your inbox.
What it does
A Hermes agent (NousResearch/hermes-agent) loads this plugin in-process and it
runs a continual Gmail triage daemon: each new message is classified by a hybrid
pre-classifier + OpenRouter LLM pipeline and gets a colored Fyxer-style
numbered Gmail label (1: To Respond … 8: Marketing). Only 1: To Respond
and 2: FYI stay in the inbox; the rest skip-inbox and archive. For
1: To Respond, the plugin asks Hermes to compose a reply draft in your
voice (drafts only, never sent). When you reply, the thread moves to
7: Actioned; when you send and are waiting, 6: Awaiting Reply.
No separate service and no public webhook — it's one plugin that loads with the agent. It can also add an optional Inbox Organizer tab to Hermes's own web dashboard for connecting/removing accounts (docs/dashboard.md).
Capabilities
- Multi-account Gmail (connect/disconnect by chatting with Hermes, or from an optional web dashboard tab — docs/dashboard.md)
- Hybrid triage: deterministic header/sender rules + LLM fallback
- Hermes-drafted replies for
1: To Respond - Sent-handling (
Actioned/Awaiting Reply) - Draft reinforcement loop — learns from draft→sent deltas: distils per-sender voice notes + global do/don't lessons + gold-example replies into a separate auditable layer that feeds future drafting briefs (in-context learning-from-edits; no model fine-tuning)
- On-demand unread rollup tool ("what needs me across my inboxes?")
- Agent tools:
inbox_create_draft,inbox_list_accounts,inbox_list_emails,inbox_get_email,inbox_get_thread,inbox_unread_rollup,inbox_connect_account/inbox_complete_connection/inbox_disconnect_account,inbox_draft_feedback_status,inbox_forget_lesson,inbox_clear_learned_notes
How it works
- Load:
register(ctx)registers the agent tools + apre_llm_callnudge hook and starts the daemon. Agateway:startuphook starts the daemon at boot (so it doesn't wait for the first agent turn). - Sync: Gmail
watch()→ Pub/Sub streaming pull (outbound connection — no webhook/tunnel) → drain history from a stored cursor → classify + label → wake Hermes to draft. A polling reconciler re-drains on a timer in case a push is dropped. - State: SQLite (
state.db) in the Hermes data volume — history cursors, draft idempotency, classified messages, thread state, draft outcomes + learned lessons (schema v3, migrates in place on deploy). OAuth tokens are AES-256-GCM encrypted at rest. - Cost: the cheap local pre-classifier handles most mail; the LLM fallback (a small OpenRouter model) runs only when needed, so per-message cost is a fraction of a cent.
Install
This is a pip / entry-point Hermes plugin — it ships the
hermes_agent.plugins entry point and your Hermes agent loads it in-process. It
is not installed with hermes plugins install <repo>: that command
git-clones a directory plugin and never installs Python dependencies, which
this plugin needs (Gmail, Pub/Sub, OpenRouter). Use pip. For the
discovery/enable model see the
Hermes plugin docs.
You need a Hermes deployment you control. The three steps below are: set up Google Cloud, install + enable the plugin, then drop in its config.
1. Google Cloud
The plugin watches Gmail over Pub/Sub via an outbound streaming pull (no webhook, no public endpoint), so it needs a Google Cloud project with the Gmail + Pub/Sub APIs, an OAuth client, and a topic + pull subscription.
- Project — console.cloud.google.com →
project selector → New Project (e.g.
hermes-inbox). - Enable APIs — APIs & Services → Library → enable Gmail API
(
gmail.googleapis.com) and Cloud Pub/Sub API (pubsub.googleapis.com). - OAuth consent screen — APIs & Services → OAuth consent screen →
External. Add scopes
gmail.modify,gmail.send,userinfo.email,userinfo.profile; add your Gmail as a Test User; then Publish App (Production audience) to avoid the 7-day refresh-token expiry (docs/oauth-modes.md). - OAuth client — Credentials → Create Credentials → OAuth client ID →
Web application. For the Authorized redirect URI use a static page
that just displays the code for you to paste back — host
oauth-callback/yourself or reusehttps://inbox-organizer.northbound.run/(it holds no secrets; the OAuth client itself must still be your own). Save the client id/secret for step 3. - Pub/Sub — Pub/Sub → Topics → Create Topic (e.g.
gmail-notifications); on that topic, Create Subscription with delivery type Pull (e.g.gmail-inbox-organizer-pull). - Let Gmail publish — on the topic → Permissions → Add Principal
gmail-api-push@system.gserviceaccount.com, role Pub/Sub Publisher. - Subscriber key — IAM & Admin → Service Accounts → create one (e.g.
hermes-inbox-pubsub), grantroles/pubsub.subscriberon the subscription (not project-wide), and download a JSON key.
(Screens and detail: docs/google-bootstrap.md.)
2. Install the plugin
Install into the same Python environment as your hermes CLI:
pip install "hermes-inbox-organizer[live]"
# or from source: pip install "hermes-inbox-organizer[live] @ git+https://github.com/Northbound-Run/hermes-inbox-organizer"
[live] pulls the Gmail/Pub/Sub/OpenRouter deps. Hermes auto-discovers the
plugin via its entry point; enable it (pip plugins are opt-in):
hermes plugins enable inbox_organizer # or add `inbox_organizer` to plugins.enabled in ~/.hermes/config.yaml
3. Config + secrets
The plugin reads its secrets as files from a config dir
(INBOX_CONFIG_DIR, default /opt/data/config) and writes its SQLite DB +
encrypted tokens to a data dir (INBOX_DATA_DIR, default
/opt/data/inbox-organizer). Point both env vars at real paths if those
defaults don't fit your host, then install these files into INBOX_CONFIG_DIR:
| File | Contents |
|---|---|
inbox-oauth-client.json |
{ "client_id", "client_secret", "redirect_uri", "owner_matrix_ids": ["@you:your-homeserver"] } (from step 1.4) |
inbox-pubsub.json |
{ "project", "topic": "projects/<id>/topics/<topic>", "subscription" } (from step 1.5) |
inbox-pubsub-sa.json |
the pubsub.subscriber service-account key (step 1.7) |
inbox-encryption-key |
32 hex bytes — openssl rand -hex 32 (AES-GCM for tokens at rest) |
And in the Hermes environment:
OPENROUTER_API_KEY— the classifier LLM (openrouter.ai/keys); most Hermes setups already have one.HERMES_API_URL(optional) — the Hermes OpenAI-compatible endpoint, needed only for the drafted replies; triage + labeling run without it.
Restart Hermes. To start the daemon at boot (instead of on the first agent
turn), copy the gateway:startup hook from
deploy/hooks/inbox-organizer-boot/ into
~/.hermes/hooks/inbox-organizer-boot/.
4. Connect a Gmail account
Two ways — same copy-paste OAuth either way:
- Chat with Hermes — "Connect a Gmail account." The agent returns a Google consent link; approve it, paste the code from the callback page back, and the account is hot-added (owner-gated). Repeat for additional mailboxes.
- Dashboard tab — open Hermes's web dashboard and use the Inbox Organizer
tab to connect/remove accounts with buttons instead of tools. If the tab isn't
there yet, run
hermes inbox-organizer install-dashboardonce and restarthermes dashboard. See docs/dashboard.md.
Verify
hermes plugins list # inbox_organizer → enabled
HERMES_PLUGINS_DEBUG=1 hermes plugins list # verbose discovery if it doesn't show up
In a running session, /plugins lists it as loaded. Ask the agent to run
inbox_list_accounts, then send yourself a test email — it should get a numbered
label within seconds (push) or a few minutes (the polling reconciler).
Deploying into a baked container image (the daemon up at boot, secrets in a read-only mount)? That's how this repo's own stack runs — see docs/setup.md for the Dockerfile + entrypoint wiring.
Layout
hermes_inbox_organizer/— the plugin package (the module), including its bundleddashboard/web-UI plugin; withtests/,deploy/(thegateway:startupboot hook), andpyproject.tomlat the repo root.oauth-callback/— the static OAuth callback page (Cloudflare Pages) used by chat onboarding.docs/— setup, Google Cloud, OAuth, sync, security, and dashboard notes.
Develop
uv venv && uv pip install -e ".[dev]"
.venv/bin/python -m pytest -q
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hermes_inbox_organizer-0.1.0.tar.gz.
File metadata
- Download URL: hermes_inbox_organizer-0.1.0.tar.gz
- Upload date:
- Size: 186.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d8703cc6f04be0769fcfc17414d64e7fe2860583e2a9bb24d6aecbd0972e13a2
|
|
| MD5 |
873d5cd21641adf440661d4d4f6563eb
|
|
| BLAKE2b-256 |
8bfe739e3edbce399ba3b7ae56abd37c520a197c4615ba646dc91e96f8dc169e
|
Provenance
The following attestation bundles were made for hermes_inbox_organizer-0.1.0.tar.gz:
Publisher:
release.yml on Northbound-Run/hermes-inbox-organizer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hermes_inbox_organizer-0.1.0.tar.gz -
Subject digest:
d8703cc6f04be0769fcfc17414d64e7fe2860583e2a9bb24d6aecbd0972e13a2 - Sigstore transparency entry: 1930536695
- Sigstore integration time:
-
Permalink:
Northbound-Run/hermes-inbox-organizer@0b3fd0fbbbba88c5c942fa5759b7729f0a341643 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Northbound-Run
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0b3fd0fbbbba88c5c942fa5759b7729f0a341643 -
Trigger Event:
push
-
Statement type:
File details
Details for the file hermes_inbox_organizer-0.1.0-py3-none-any.whl.
File metadata
- Download URL: hermes_inbox_organizer-0.1.0-py3-none-any.whl
- Upload date:
- Size: 139.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0ff95d07f16d4d4747d1343c025fbea770f3f6c902629480ef72e939ae9816fd
|
|
| MD5 |
5eafde14226a9941fc6197c650b2c33d
|
|
| BLAKE2b-256 |
146001691d3ea7d4d7da198b3c0f8f2bf557ec3b0eea3f481a52c2eab58fb02c
|
Provenance
The following attestation bundles were made for hermes_inbox_organizer-0.1.0-py3-none-any.whl:
Publisher:
release.yml on Northbound-Run/hermes-inbox-organizer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hermes_inbox_organizer-0.1.0-py3-none-any.whl -
Subject digest:
0ff95d07f16d4d4747d1343c025fbea770f3f6c902629480ef72e939ae9816fd - Sigstore transparency entry: 1930536786
- Sigstore integration time:
-
Permalink:
Northbound-Run/hermes-inbox-organizer@0b3fd0fbbbba88c5c942fa5759b7729f0a341643 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Northbound-Run
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0b3fd0fbbbba88c5c942fa5759b7729f0a341643 -
Trigger Event:
push
-
Statement type: