Hermes Agent plugin that records every gateway chat message — any platform — to an Obsidian-style Markdown vault, with voice transcripts and image descriptions.
Project description
hermes-chat-recorder — Hermes Plugin
Record every chat message the Hermes gateway sees — on any connected platform (Matrix, Telegram, Discord, Slack, Signal, WhatsApp, IRC, email, …) — into an Obsidian-style Markdown vault. Voice notes are transcribed and images are described inline using whatever STT and vision providers Hermes is already configured for. No databases, no sidecar indexes, no third-party API keys.
hermes-chat-recorder adds one thing to Hermes: a single pre_gateway_dispatch hook that archives traffic to a greppable vault. It is recording-only — it never decides whether the agent wakes up.
Status: alpha (pre-1.0). APIs and config schema may change. Used in production at Northbound.
Why this exists
Most chat-archival setups fail in one of two boring ways: they bolt onto a single platform, or they drag in their own transcription/vision stack with its own keys to rotate. This plugin is delegation-based instead:
- Any platform, one hook. Every Hermes adapter normalizes to a unified
MessageEvent, so the same recorder captures Matrix, Telegram, Discord, Slack, Signal, WhatsApp, IRC, and email without per-platform code. - No third-party deps. STT and vision are delegated to Hermes's built-in tools — local faster-whisper, Groq, OpenAI, Mistral, or xAI for speech; the main LLM or an auxiliary vision provider for images. The only runtime requirement is Hermes itself.
- Recording-only. It does NOT gate the agent's wake — use Hermes's native mention/allowlist settings for that. It records before Hermes's auth/pairing checks, so messages from unpaired senders land in the vault too. Treat the vault path as sensitive.
- Pretty names for free. Folders come out as
telegram/Family-Chat/and headers as### 09:14 Annika · voice (0:12), resolved from adapter metadata (and, on Matrix, live room/profile lookups). - Greppable, not a database. Per-platform, per-chat, per-day Markdown files with HTML-comment anchors for idempotency. Obsidian-friendly, diff-friendly, no migrations.
Quick Start
# 1) Install + enable the plugin (pick ONE path)
# A. Hermes plugin manager — git-clones into ~/.hermes/plugins/chat_recorder
hermes plugins install Northbound-Run/hermes-chat-recorder --enable
# B. PyPI — auto-discovered via the hermes_agent.plugins entry point
pip install hermes-chat-recorder
hermes plugins enable chat_recorder
# 2) Configure: add a chat_recorder block to your Hermes config.yaml.
# The only setting you really want is vault_root (defaults to
# /data/vault/transcripts if omitted).
plugins:
enabled:
- chat_recorder # `--enable` / `enable` already added this
chat_recorder:
enabled: true
vault_root: /data/vault/transcripts
record_outbound: true
timezone: America/Los_Angeles
# 3) Restart the gateway so the plugin loads and wires the adapters.
# (Recording begins with the first message after startup.)
# 4) Verify it registered
hermes plugins list # chat_recorder should be listed + enabled
Notes:
- Path A clones this repo into
~/.hermes/plugins/chat_recorder/(the directory name comes from the manifestname:, which is why--enableaddschat_recorder). Without--enablethe installer prompts "Enable now? [y/N]". - Path B installs from PyPI; Hermes discovers it through the
hermes_agent.pluginsentry-point group. Use this when the plugin lives in the same venv as Hermes. - Don't run both paths at once. If a directory clone and the PyPI package are both present they collide on the
chat_recorderkey, and Hermes loads the entry-point (PyPI) copy — entry-points are merged last, so they win the collision and the clone is silently ignored. Pick the one that matches how you deploy Hermes. - The vault holds unredacted message content (including from unpaired senders, recorded before auth). Put
vault_rootsomewhere access-controlled. - Python 3.10+ is required. The plugin pulls in no third-party runtime dependencies.
Updating
# Path A (plugin manager)
hermes plugins update chat_recorder
# Path B (PyPI)
pip install -U hermes-chat-recorder
Restart the gateway afterward so the updated plugin code is reloaded. See Upgrading from ≤ 0.6.x if you are crossing the v0.7.0 vault-layout change.
Documentation
docs/DESIGN.md— full architecture: event normalization, the recorder pipeline, adapter wiring, name resolution, and failure policy.CHANGELOG.md— version history (Keep a Changelog format; pre-1.0 minor bumps may break).- Vault format — the on-disk Markdown layout and idempotency model.
- Compatibility notes & gotchas — adapter mention-gating, plugin load timing, and outbound-capture caveats.
- Hermes plugin guide — upstream reference for plugin discovery and debugging.
What gets recorded
| Inbound | Behaviour |
|---|---|
| Text | Append the message to vault_root/<platform>/<chat>/<YYYY-MM-DD>.md. Pass through unmodified — Hermes's normal wake settings decide whether the agent replies. |
| Voice note | Transcribe via Hermes's built-in STT (tools.transcription_tools). Append placeholder + transcript section. Rewrite event.text to the transcript so the agent has usable content if it wakes. |
| Image / sticker | Describe via Hermes's built-in vision (tools.vision_tools). Append placeholder + description section. Rewrite event.text to caption + description + OCR'd text. |
| Video / file / location | Recorded as-is (caption + media provenance), no processing. |
| Reaction | Ignored (not recorded as a message section). |
| Outbound (the agent's own replies, every platform) | Recorded with a reply_to: link back to the trigger when available. |
Configuration
The plugin reads the plugins.chat_recorder block in your Hermes config.yaml:
plugins:
enabled:
- chat_recorder
chat_recorder:
enabled: true
vault_root: /data/vault/transcripts
record_outbound: true
timezone: America/Los_Angeles
# Optional: restrict recording to specific platforms.
# Empty / omitted = record everything the gateway dispatches.
platforms: [] # e.g. [matrix, telegram]
# Optional: display name for the bot's own outbound sections.
# Default resolves via the bot's Matrix profile, else "bot".
bot_name: ""
# "group" (default) keeps the per-platform/per-chat subfolders.
# "1on1" assumes the bot only ever lives in a single DM and
# flattens to <vault_root>/<YYYY-MM-DD>.md — no subfolders.
bot_type: "group"
# Optional: manual name overrides, keyed by raw platform IDs.
# Useful when a Signal/WhatsApp bridge user has no display name,
# or to rename a chat's folder. Overrides win over all lookups.
name_overrides:
rooms:
"!abcdef1234:example.org": "Family Signal"
users:
"@signal_2c991545-...:example.org": "Annika"
STT and vision are configured at the Hermes top level, not here:
stt:
enabled: true
provider: "local" # or "groq" / "openai" / "mistral" / "xai"
local:
model: "base"
auxiliary:
vision:
provider: "main" # use the main LLM, or override per Hermes docs
model: ""
Optional env vars:
TRANSCRIPT_TZ— overridestimezone.
Vault format
Per-platform, per-chat, per-day files at
<vault_root>/<platform>/<chat-slug>/<YYYY-MM-DD>.md, sections
separated by ---, each anchored by an HTML comment:
<!-- event:$abcd1234:server -->
### 09:14 Annika · voice (0:12) · stage:transcribed
**mxc:** mxc://server/abcdef
**duration_sec:** 12
> okay so the deck was titled "what ai can do for your business" and
> we showed it to the meridian team last thursday
---
<!-- event:$efgh5678:server -->
### 09:14 Recorder · reply · stage:sent
**reply_to:** $abcd1234:server
You bet — the AI-for-business one. I'll drop a refresher in your daily note.
---
Idempotency comes from the HTML-comment anchor — re-delivery of the same event ID finds the existing section and either updates its stage in-place or no-ops if already at a terminal stage. The vault stays greppable and Obsidian-friendly: no databases, no sidecar indexes.
Upgrading from ≤ 0.6.x
v0.7.0 added the platform folder level. Existing Matrix-only vaults move with one command:
mkdir -p <vault_root>/matrix && mv <vault_root>/<each-room-folder> <vault_root>/matrix/
(bot_type: "1on1" flat layouts are unaffected.)
Compatibility notes & gotchas
Adapter-level mention gating hides messages from the recorder.
Some Hermes adapters can drop unmentioned group messages inside the
adapter, before any plugin hook runs — e.g. Signal's
require_mention / SIGNAL_REQUIRE_MENTION. With that enabled, gated
messages are never dispatched and therefore never recorded. If you
want "archive everything, reply only when mentioned", leave
adapter-level gating off and gate the wake instead: a small
companion plugin that returns {"action": "skip"} from
pre_gateway_dispatch for unmentioned group messages. Hermes runs all
hook callbacks before acting on any result, so the recorder archives
the message either way. (Caveat for entry-point distribution: the
gateway honors the first skip and stops at a rewrite, so a gate
plugin should load before this one — install it as a user-dir plugin
under ~/.hermes/plugins/, which always loads ahead of pip
entry-points.)
Plugin load timing on older Hermes. Current Hermes calls
discover_plugins() at gateway startup, so recording begins with the
first message. Older versions loaded entry-point plugins lazily on the
first agent turn — messages before that were invisible to the hook. On
such versions, add a gateway:startup hook that calls
hermes_cli.plugins.discover_plugins() (see Hermes's gateway event
hooks docs) to load plugins at boot.
Outbound capture starts at the first inbound dispatch. The send
wrappers are wired lazily when the first message flows through the
gateway, so bot messages sent before that moment in a fresh process
(e.g. startup broadcasts) may be missed or, when no chat name is known
yet, filed under an ID-derived folder name until a message in that
chat establishes the pretty name.
Troubleshooting
Plugin not showing up? Hermes has verbose discovery logs:
HERMES_PLUGINS_DEBUG=1 hermes plugins list
Common causes: the plugin isn't in plugins.enabled, or the gateway
wasn't restarted after install. See the
Hermes plugin guide
for the full debugging walkthrough.
Local development
git clone https://github.com/Northbound-Run/hermes-chat-recorder
cd hermes-chat-recorder
pip install -e .[dev]
pytest -q # 300+ unit tests, no network
ruff check .
mypy src
The test suite runs entirely offline. pytest uses
--import-mode=importlib (set in pyproject.toml) so the repo-root
__init__.py — the directory-install entry shim — doesn't interfere
with test collection.
Project layout
.
├── plugin.yaml Manifest read by the `hermes plugins install` git-clone path
├── __init__.py Repo-root entry shim: puts src/ on sys.path, re-exports register()
├── pyproject.toml Package metadata, hermes_agent.plugins entry point, tooling
├── README.md
├── CHANGELOG.md
├── LICENSE
├── docs/
│ └── DESIGN.md Full architecture & design rationale
├── src/
│ └── hermes_chat_recorder/
│ ├── __init__.py Public surface — register()
│ ├── plugin.py register(ctx): config load + hook/adapter wiring
│ ├── recorder.py Per-event orchestration (placeholder → finalize)
│ ├── writer.py Vault file writer: sections, anchors, idempotency
│ ├── events.py MessageEvent → typed EventInfo normalization
│ ├── transcriber.py Voice notes → Hermes STT
│ ├── describer.py Images → Hermes vision
│ ├── name_resolver.py Chat/user pretty-name resolution + cache
│ ├── config.py Config block parsing & validation
│ ├── types.py Shared dataclasses / enums
│ ├── _background_loop.py Async helper for off-thread work
│ └── plugin.yaml Manifest (wheel copy; informational on the pip path)
└── tests/ pytest suite
Two manifests are intentional: the repo-root plugin.yaml is what
the git-clone installer reads, while the packaged copy under
src/ ships in the wheel for reference (the pip/entry-point path
builds its manifest from the entry-point name and doesn't parse it).
Keep them in sync.
Architecture in one paragraph
The plugin registers a single pre_gateway_dispatch hook. Inbound, it
normalizes Hermes's unified MessageEvent (any platform) into a typed
EventInfo, writes a placeholder section, runs voice/image events
through Hermes's STT/vision tools, finalizes the section, and rewrites
event.text so the agent sees the transcript/description. On the
first dispatch it also wraps send on every live platform adapter
so outbound replies land in the vault; the Matrix adapter additionally
contributes its client for name lookups and a media-download fallback.
Failure policy: per-event errors degrade to *_failed sections;
startup/config errors fail registration loudly. See
docs/DESIGN.md for the full design.
License
MIT — Copyright (c) 2026 Northbound.
Related
- Hermes Agent — the agent runtime this plugin extends (docs).
- hermes-chat-recorder on PyPI — the published package for the pip install path.
Project details
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_chat_recorder-0.7.3.tar.gz.
File metadata
- Download URL: hermes_chat_recorder-0.7.3.tar.gz
- Upload date:
- Size: 74.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a373360dbc8532e6475488554537cd5f3968c403c3d02cb1c66bdfadf92cc1c0
|
|
| MD5 |
40f75010f2addacf23e0a80de5d70419
|
|
| BLAKE2b-256 |
7587417a266bba713cf04d7c649dd991eb43859c75a3ea9c8f7a901195ccee2f
|
Provenance
The following attestation bundles were made for hermes_chat_recorder-0.7.3.tar.gz:
Publisher:
publish.yml on Northbound-Run/hermes-chat-recorder
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hermes_chat_recorder-0.7.3.tar.gz -
Subject digest:
a373360dbc8532e6475488554537cd5f3968c403c3d02cb1c66bdfadf92cc1c0 - Sigstore transparency entry: 1932493487
- Sigstore integration time:
-
Permalink:
Northbound-Run/hermes-chat-recorder@85a8f48126e0d427ae8f7162fa41a83c56042f74 -
Branch / Tag:
refs/tags/v0.7.3 - Owner: https://github.com/Northbound-Run
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@85a8f48126e0d427ae8f7162fa41a83c56042f74 -
Trigger Event:
release
-
Statement type:
File details
Details for the file hermes_chat_recorder-0.7.3-py3-none-any.whl.
File metadata
- Download URL: hermes_chat_recorder-0.7.3-py3-none-any.whl
- Upload date:
- Size: 46.6 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 |
3f797a0460dd4466513369f8db86f90f4e51f0f4a52d17aa5a81cfb6140f4d0b
|
|
| MD5 |
e4b4b4de0248bd2bdf8fa1a5ae63b78c
|
|
| BLAKE2b-256 |
b0699dbe32b79f5d031648fbf2f3dc23b04a492b389478def1e0caa5c19fa87b
|
Provenance
The following attestation bundles were made for hermes_chat_recorder-0.7.3-py3-none-any.whl:
Publisher:
publish.yml on Northbound-Run/hermes-chat-recorder
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hermes_chat_recorder-0.7.3-py3-none-any.whl -
Subject digest:
3f797a0460dd4466513369f8db86f90f4e51f0f4a52d17aa5a81cfb6140f4d0b - Sigstore transparency entry: 1932493696
- Sigstore integration time:
-
Permalink:
Northbound-Run/hermes-chat-recorder@85a8f48126e0d427ae8f7162fa41a83c56042f74 -
Branch / Tag:
refs/tags/v0.7.3 - Owner: https://github.com/Northbound-Run
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@85a8f48126e0d427ae8f7162fa41a83c56042f74 -
Trigger Event:
release
-
Statement type: