--- title: 'Hermes: the plugins and their hooks' url: https://blog.guigpap.com/en/hermes/plugins/ url_md: https://blog.guigpap.com/en/hermes/plugins.md category: hermes date: '2026-08-04' maturite: production techno: - telegram - docker application: - ai - operations --- # Hermes: the plugins and their hooks > Four plugins grafted onto the Hermes lifecycle — voice transcription, editable status message, Codex quota alert and Mnemosyne memory ## 1. What? — Definition and context [Skills](/en/hermes/skills/) are text: they steer the model. **Plugins** are Python code: they run, whatever the model decides. A plugin registers on **hooks** of the [Hermes](/en/hermes/) lifecycle and can, depending on the hook, observe an event, rewrite a message before processing, or expose new tools to the agent. ### The four active plugins | Plugin | Version | Contribution | Author | |--------|---------|--------|--------| | `telegram-voice-transcriptor` | 1.1.0 | `pre_gateway_dispatch` hook | Custom | | `telegram-message-editor` | 1.0.0 | `telegram_status_message` tool | Custom | | `codex-usage-alert` | 1.0.0 | `post_api_request` hook | Custom | | `mnemosyne` | 0.4.0 | 20 tools + 3 hooks | Third party (Abdias J) | ### The hooks used | Hook | Moment | Can do | |------|--------|-----------| | `pre_gateway_dispatch` | Message received, before processing | Rewrite or redirect the event | | `pre_llm_call` | Before the model call | Inject context into the prompt | | `on_session_start` | Session opening | Load an initial state | | `post_tool_call` | After a tool call | Observe, capture | | `post_api_request` | After a successful LLM call | Observe, trigger a side effect | > **Caution - User plugins are opt-in** > > Nothing loads without appearing in the `plugins.enabled` allow-list of the config file. That is deliberate: dropping a directory into `/opt/data/plugins/` is not enough to activate it. A plugin left in the directory stays inert. --- ## 2. Why? — Stakes and motivations ### Why user plugins rather than patching the core? The application tree `/opt/hermes` belongs to the image. A change there survives until the next update — and then disappears without a sound, which is the worst possible ending for a customisation. User plugins live in `/opt/data/plugins/`, that is, in the bind mount. They survive `docker compose up --force-recreate` as well as image updates, and go into the [daily backup](/en/infrastructure/database-backup/). It is the same rule as [Mnemosyne's wrapper mode](/en/hermes/memoire-mnemosyne/): **everything meant to last lives in the volume, never in the image**. ### Plugin or MCP tool? Both add capabilities, but not in the same place nor at the same price. | | N8N MCP tool | Plugin | |---|---|---| | **Where** | A node in a workflow | Python inside the container | | **Access** | Whatever N8N can reach | The Hermes runtime itself | | **Change** | Edit the workflow, restart the container | Edit the file, restart the container | | **Can react to an event** | No — only be called | Yes, through hooks | The dividing line is simple: a business action becomes an [MCP tool](/en/hermes/outils-mcp/); a behaviour that must fire **without the agent deciding** becomes a plugin. --- ## 3. How? — Technical implementation ### `telegram-voice-transcriptor` On a Telegram voice message, the plugin rewrites the event to inject the block of the `transcriptor-fr-voice` skill. The STT pipeline then prefixes the transcript as a quote, and the agent replies with the cleaned-up French text. Its interest lies less in what it does than in **what it refuses to fire on**. > **Tip - The trigger keys on type, never on content** > > The entry condition is `MessageType.VOICE`, a value the Telegram adapter only sets from `message.voice`. Typed text, a file caption, an audio attachment, or text imitating a transcript never match. > > That is a trust-model choice: if the trigger keyed on message content, anyone able to write to the bot could activate a rewrite of their own message. The injected activation note goes the same way — it explicitly states not to follow or execute the source text, only to apply the skill's output contract. The hook is also **fail-open**: any exception is logged as a warning and the message goes on to normal processing. A broken skill or a failed import must never swallow a voice message. ### `telegram-message-editor` This plugin exposes one tool, `telegram_status_message`, with two actions: `create` sends a message and returns its `message_id`, `update` edits that same message in place. 4096-character cap, and by default the current conversation when the call comes from Telegram. The problem it solves is an ergonomics problem. An agent working for several minutes on a multi-step task has a choice between staying silent — and looking stuck — or sending one message per step, and drowning the conversation. In-place editing gives a third option: a single dashboard that updates itself. > **Note - Same need, two implementations** > > The [Codex Progress Handler](/en/workflows/codex-cli-integration/) solves exactly this problem on the N8N side, with a buffer Data Table and a throttle. Here, the agent simply calls a tool when it judges a status refresh useful. > > The difference is structural: on the N8N side the workflow decides the rhythm, on the Hermes side it is the agent. It is the same orchestration gap that separates the [two Telegram agents](/en/workflows/systeme-conversationnel/). ### `codex-usage-alert` Hermes shares the ChatGPT subscription with CLI Ollama and the N8N workflows. Hitting the weekly cap produces no signal until the bot starts answering with errors. The plugin registers on `post_api_request`, reacts only to `openai-codex` provider calls, and alerts when account consumption crosses a threshold (90% by default). Its four guardrails are worth noting, because they are what separates an acceptable observation plugin from one that degrades the service: | Guardrail | Implementation | |-----------|---------------| | **Non-blocking** | The hook starts at most one short daemon thread and hands control back immediately | | **Controlled load** | A 300 s cooldown avoids calling the backend again on every message of an active conversation | | **Anti-spam** | One alert per threshold + quota window + reset time combination, with history bounded to 200 keys | | **Fail-open** | Any error is logged and never affects the Hermes response | The common principle: a plugin grafted onto a hot path must never be what breaks or slows the response. ### `mnemosyne` The only third-party plugin of the set, and by far the largest: 20 tools and 3 hooks (`pre_llm_call`, `on_session_start`, `post_tool_call`). It is installed in wrapper mode and has a [dedicated article](/en/hermes/memoire-mnemosyne/). Its presence in this list says something about the extension system: long-term memory, arguably the agent's most structuring feature, installs through the same mechanism as a quota alert. ### Deployment A plugin is a directory with a `plugin.yaml` and an `__init__.py` exposing `register(ctx)`. ```python def register(ctx) -> None: ctx.register_hook("post_api_request", _on_post_api_request) ``` ```yaml name: codex-usage-alert version: 1.0.0 description: "Non-blocking Telegram Home alert when OpenAI Codex account usage crosses a threshold." author: Guillaume PARRAT + Hermes Agent provides_hooks: - post_api_request ``` Only `telegram-voice-transcriptor` has versioned source in the repository; deployment happens by copy then restart: ```bash cp -r ai-stack/hermes/plugins/telegram-voice-transcriptor ai-stack/hermes/data/plugins/ docker compose -f ai-stack/docker-compose.yaml restart hermes docker exec hermes hermes plugins list # expected status: enabled ``` --- ## 4. What if? — Outlook and limits ### Current limits | Limit | Impact | Mitigation | |--------|--------|------------| | **Three plugins out of four unversioned** | Only the transcriptor has source in the repository; the others exist only in production | Daily backup; porting to the repository still to do | | **Restart required** | Any change forces a container restart | Batch the changes | | **No automated test** | A broken plugin is discovered in use | Fail-open limits the damage to one lost feature | | **Hooks unversioned upstream** | An upstream signature change breaks a plugin | `hermes plugins list` after every image update | | **No isolation** | A plugin runs inside the gateway process | Discipline: non-blocking and fail-open by default | ### Evolution scenarios **If plugins must be versioned**: - `ai-stack/hermes/plugins/` already exists and holds the transcriptor — the other three follow the same pattern. - Main benefit: being able to roll back after an upstream update that breaks a hook. **If a plugin must become blocking**: - The typical case would be a real approval chain before sensitive actions, rather than the current system prompt rule. - A `pre_tool_call` hook would fit — but a blocking plugin on the hot path needs an explicit timeout and fallback behaviour, otherwise a plugin failure freezes the agent. **If observability must improve**: - `post_api_request` and `post_tool_call` are the natural points to export metrics. - A plugin pushing a counter to Prometheus would give Hermes the same visibility as the rest of the stacks in [Grafana](/en/infrastructure/monitoring-stack/). --- ## Related pages ### Hermes - [Hermes Agent](/en/hermes/) — The deployment and plugin activation - [Mnemosyne memory](/en/hermes/memoire-mnemosyne/) — The largest plugin of the set - [Skills](/en/hermes/skills/) — `hermes-user-plugins` and `hermes-runtime-plugins`, the writing toolkit - [MCP tools](/en/hermes/outils-mcp/) — The other way to add a capability ### Workflows - [Codex CLI Integration](/en/workflows/codex-cli-integration/) — In-place editing, N8N version - [Voice Transcription](/en/workflows/voice-transcription/) — The other transcription path ### Infrastructure - [Database backup](/en/infrastructure/database-backup/) — Deployed plugins are backed up ## Métadonnées agent - Cet article est issu du blog GuiGPaP Lab. - Contexte global du blog: https://blog.guigpap.com/llms.txt - Contact auteur: https://odoo.guigpap.com/mon-cv - Licence: CC-BY-SA 4.0