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
Quick Start
You need a Hermes deployment you control and a Google Cloud project (a one-time setup in your browser). Then:
# 1) Install (pip — this plugin needs the Gmail/Pub/Sub deps)
pip install "hermes-inbox-organizer[live]"
hermes plugins enable inbox_organizer
# 2) One-time Google Cloud setup (OAuth client + Pub/Sub)
# → docs/google-bootstrap.md (~10 min, in your browser)
# 3) Configure — the wizard writes the config files + keys
# (auto-generates the encryption key; never prints secrets)
hermes-inbox-organizer setup
hermes-inbox-organizer status # what's configured + capabilities
# 4) Restart Hermes, then in chat: "connect a Gmail account"
Already run a Hermes agent? Hand it
HERMES_SELF_INSTALL.md and it'll do all of this for
you. The full detail and the by-hand path are under Install.
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
The Quick Start is the short version; this is the full detail,
including the by-hand config path if you'd rather not use the setup wizard.
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.
Easiest — the setup wizard. It prompts for the values from step 1, generates
the encryption key, and writes everything into INBOX_CONFIG_DIR (mode 0600):
hermes-inbox-organizer setup # or: python -m hermes_inbox_organizer setup
hermes-inbox-organizer status # verify what's configured + capabilities
The wizard writes env-only values (the classifier API key + endpoint, optional
HERMES_API_URL) to INBOX_CONFIG_DIR/inbox.env for you to wire into the Hermes
environment (e.g. a compose env_file:).
Or by hand — 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) |
owner_matrix_ids is the owner allowlist for the connect/disconnect tools —
gateway sender ids, not Matrix-only despite the name (use whatever
source.user_id your channel reports). You can instead set it via
INBOX_OWNER_MATRIX_IDS (comma-separated), which takes precedence. The gate is
fail-closed: with neither set, even the owner can't connect a mailbox. Proactive
module pushes (2FA, shipping) go to your Hermes /sethome home channel on
whatever platform you use.
And in the Hermes environment:
- Classifier LLM — the classifier speaks the OpenAI API, so it works with
any compatible endpoint. By default it uses OpenRouter via
OPENROUTER_API_KEY(openrouter.ai/keys; most Hermes setups already have one). For any other endpoint — a local vLLM/Ollama/LM Studio server, or another gateway — setINBOX_CLASSIFIER_API_KEYandINBOX_CLASSIFIER_BASE_URLinstead, and optionallyINBOX_CLASSIFIER_MODEL(defaultgoogle/gemini-2.5-flash-lite).INBOX_CLASSIFIER_API_KEYtakes precedence overOPENROUTER_API_KEY. 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-inbox-organizer status # config + capabilities at a glance
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.
Updating
pip install --upgrade "hermes-inbox-organizer[live]"
Then restart Hermes (or /reset in a CLI session) so the updated tools + schemas
reload. The SQLite schema migrates in place on load, so cursors, tokens, and
learned drafting state carry over; run hermes-inbox-organizer status to confirm.
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 # tests run fully offline (network/Google/agent are seamed)
ruff check . # lint
mypy # type-check
This plugin follows the shared Hermes Plugin Standard (packaging, tooling, CI, release, and security conventions used across our Hermes plugins). See CONTRIBUTING.md before opening a PR.
License
MIT — see LICENSE.
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.3.0.tar.gz.
File metadata
- Download URL: hermes_inbox_organizer-0.3.0.tar.gz
- Upload date:
- Size: 200.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
482ef3295621a3a3f472c63aaf510dbf9953060356b92cb5c5eba58c733c7fd5
|
|
| MD5 |
e994fa158aa3ece7e4d9f0ac6023c851
|
|
| BLAKE2b-256 |
68b0036b05cf39c8d68bfc9e39326af9dada3f82ee135cd4ef527a6ac4df0cda
|
Provenance
The following attestation bundles were made for hermes_inbox_organizer-0.3.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.3.0.tar.gz -
Subject digest:
482ef3295621a3a3f472c63aaf510dbf9953060356b92cb5c5eba58c733c7fd5 - Sigstore transparency entry: 2086618395
- Sigstore integration time:
-
Permalink:
Northbound-Run/hermes-inbox-organizer@b371dce8c12cf89228a9589949c0313662f309bd -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/Northbound-Run
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b371dce8c12cf89228a9589949c0313662f309bd -
Trigger Event:
push
-
Statement type:
File details
Details for the file hermes_inbox_organizer-0.3.0-py3-none-any.whl.
File metadata
- Download URL: hermes_inbox_organizer-0.3.0-py3-none-any.whl
- Upload date:
- Size: 148.4 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 |
cc3b753d230e59535b4d77be40bde9fa48f063bc92af4eafc93649fdfd1eeb0a
|
|
| MD5 |
e3a3d1f3cbb6d3219a5a1bca18fbbb9e
|
|
| BLAKE2b-256 |
9c0fc4d668e67c119cc4e3af355beb732c87ec76e64b00733afc16a24d15a81c
|
Provenance
The following attestation bundles were made for hermes_inbox_organizer-0.3.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.3.0-py3-none-any.whl -
Subject digest:
cc3b753d230e59535b4d77be40bde9fa48f063bc92af4eafc93649fdfd1eeb0a - Sigstore transparency entry: 2086619204
- Sigstore integration time:
-
Permalink:
Northbound-Run/hermes-inbox-organizer@b371dce8c12cf89228a9589949c0313662f309bd -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/Northbound-Run
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b371dce8c12cf89228a9589949c0313662f309bd -
Trigger Event:
push
-
Statement type: