Skip to main content

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

PyPI CI Python License: MIT

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 manifest name:, which is why --enable adds chat_recorder). Without --enable the installer prompts "Enable now? [y/N]".
  • Path B installs from PyPI; Hermes discovers it through the hermes_agent.plugins entry-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_recorder key, 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_root somewhere 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 — overrides timezone.

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

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

hermes_chat_recorder-0.7.3.tar.gz (74.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hermes_chat_recorder-0.7.3-py3-none-any.whl (46.6 kB view details)

Uploaded Python 3

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

Hashes for hermes_chat_recorder-0.7.3.tar.gz
Algorithm Hash digest
SHA256 a373360dbc8532e6475488554537cd5f3968c403c3d02cb1c66bdfadf92cc1c0
MD5 40f75010f2addacf23e0a80de5d70419
BLAKE2b-256 7587417a266bba713cf04d7c649dd991eb43859c75a3ea9c8f7a901195ccee2f

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file hermes_chat_recorder-0.7.3-py3-none-any.whl.

File metadata

File hashes

Hashes for hermes_chat_recorder-0.7.3-py3-none-any.whl
Algorithm Hash digest
SHA256 3f797a0460dd4466513369f8db86f90f4e51f0f4a52d17aa5a81cfb6140f4d0b
MD5 e4b4b4de0248bd2bdf8fa1a5ae63b78c
BLAKE2b-256 b0699dbe32b79f5d031648fbf2f3dc23b04a492b389478def1e0caa5c19fa87b

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page