🧩 Hermes Plugin System — Complete Guide
“The core is a narrow waist; capability lives at the edges.” — Hermes Agent design philosophy
đź“‹ Table of Contents
- What Are Plugins?
- Plugin Architecture
- Installed Plugins
- How to Install a New Plugin
- Configuration Reference
- Lifecycle Hooks Reference
- Troubleshooting
- Related Notes
What Are Plugins?
Plugins are the primary way to extend Hermes Agent without modifying the core codebase. They allow you to add new commands, filter messages, transform responses, and integrate external services — all while staying sandboxed from the core agent.
Key principles:
- Plugins are opt-in — only plugins listed in
plugins.enabledare loaded - Each plugin lives in its own directory with a
plugin.yamlmanifest +__init__.py - Plugins register hooks to intercept various stages of the agent lifecycle
- There are 4 sources of plugins (see below)
Plugin Sources (Priority Order)
| # | Source | Path | Purpose |
|---|---|---|---|
| 1 | Bundled | <repo>/plugins/<name>/ | Shipped with Hermes Agent (browser, image_gen, web, etc.) |
| 2 | User | ~/.hermes/plugins/<name>/ | Custom plugins you create or install |
| 3 | Project | ./.hermes/plugins/<name>/ | Project-specific plugins (opt-in via HERMES_ENABLE_PROJECT_PLUGINS) |
| 4 | Pip | via hermes_agent.plugins entry point | Python packages that expose plugins |
Later sources override earlier ones on name collision.
Plugin Architecture
Directory Structure
Every plugin must have:
~/.hermes/plugins/<plugin-name>/
├── plugin.yaml # Manifest (REQUIRED)
└── __init__.py # Python code (REQUIRED)
plugin.yaml
The manifest declares metadata and hooks:
name: my-plugin # Unique identifier
version: 1.0.0 # Version string
description: "What it does"
author: "Your name"
kind: standalone # standalone | platform | backend | exclusive | model-provider
hooks: # Which lifecycle hooks to register
- pre_gateway_dispatch
requires_env: [] # Optional: required env varsinit.py
The code file must expose a register(ctx) function:
"""My plugin — does something useful."""
import logging
logger = logging.getLogger(__name__)
def register(ctx) -> None:
"""Register hooks."""
ctx.register_hook("pre_gateway_dispatch", my_handler)
logger.info("my-plugin: registered")The ctx object (PluginContext) provides:
ctx.register_hook(name, handler)— register a lifecycle hookctx.register_tool(...)— register a new toolctx.llm— access to the host-owned LLMctx.profile_name— current profile name
How Plugins Are Loaded
On every gateway startup, Hermes:
- Scans all 4 plugin sources for
plugin.yamlfiles - Parses manifests into
PluginManifestobjects - Filters by
plugins.enabledin config.yaml (only enabled plugins are loaded) - Imports each enabled plugin’s
__init__.py - Calls
register(ctx)which registers the plugin’s hooks/tools - Logs success or errors in
~/.hermes/logs/agent.log
Installed Plugins
1. model-switcher
| Field | Value |
|---|---|
| Hook | pre_gateway_dispatch |
| Commands | /gpt → GPT models, /pro → DeepSeek Pro, /flash → back to Flash |
| Purpose | Switch AI models mid-conversation without restarting |
How it works: Intercepts messages starting with /gpt, /pro or /flash, rewrites them to set a session-scoped model override, then continues dispatch normally. The model switch lasts for the chat session or until changed.
2. balance-check
| Field | Value |
|---|---|
| Hook | pre_gateway_dispatch |
| Commands | /balance |
| Purpose | Check remaining DeepSeek API credit balance |
How it works: Intercepts /balance command, runs the DeepSeek balance check script, and returns the result as a formatted response. No LLM call needed — direct API query.
3. vps-status
| Field | Value |
|---|---|
| Hook | pre_gateway_dispatch |
| Commands | /status |
| Purpose | Check VPS health (CPU, memory, disk, uptime) |
How it works: Intercepts /status command, runs system monitoring commands, returns a formatted status report. Useful for quick server health checks from WhatsApp.
4. public-mode
| Field | Value |
|---|---|
| Hook | pre_gateway_dispatch + transform_llm_output |
| Commands | None (authorization layer) |
| Purpose | Allowlist-based access control + response scanner for approved guests |
Two-layer protection:
Layer 1 — Incoming (pre_gateway_dispatch):
- Unknown senders → ❌ silently dropped
- Approved guests → ✅ message rewritten with public mode instructions + public info document
- Christian (owner) → ✅ full access, no interference
Layer 2 — Outgoing (transform_llm_output):
- Scans every response before it reaches the guest
- Blocks forbidden content: health data, VPS/server info, personal relationships, API keys, commands, system messages
- Replaces blocked content with a safe fallback response
ID lists (in __init__.py):
CHRISTIAN_IDS = [...] # Full access
APPROVED_GUEST_IDS = [...] # Public mode onlyNote: The WhatsApp bridge uses LID format (e.g., 123456789012345@lid) rather than raw phone numbers. Both formats should be included.
5. guest-manager
| Field | Value |
|---|---|
| Hook | pre_gateway_dispatch |
| Commands | /guests add, /guests remove, /guests list |
| Purpose | Manage WhatsApp guest access control — CLI script + chat interface |
How it works: Intercepts /guests <add|remove|list> commands and runs the manage-guest.sh script. The script edits both the .env bridge allowlist and the public-mode plugin ID lists in one go. Includes automatic backup + Python syntax check with rollback.
Backing script: ~/.hermes/scripts/manage-guest.sh
| Subcommand | Example |
|---|---|
/guests add <number> <name> <role> [lid] | /guests add 6282XXXXXXXXXX "Nama" guest |
/guests remove <number> | /guests remove 6282XXXXXXXXXX |
/guests list | Shows both Gate 1 (.env) and Gate 2 (plugin) allowlists |
Note: After adding/removing, you need to restart the gateway (manage-guest restart or hermes gateway restart).
6. garden-manager
| Field | Value |
|---|---|
| Hook | pre_gateway_dispatch |
| Commands | /garden add, /garden edit, /garden delete |
| Purpose | Manage digital garden notes directly from chat |
How it works: Intercepts /garden add|edit|delete messages and rewrites them as structured tasks ([GARDEN ADD], [GARDEN EDIT], [GARDEN DELETE]) for the agent. The agent handles all AI reasoning — interpreting instructions, choosing folders, adding wikilinks, updating indexes, and gating content through publish-garden.sh.
The plugin is lightweight by design — no privacy scan at the gateway level (since the user sends instructions, not raw note content). The real privacy gate is publish-garden.sh which runs the scanner before every commit.
| Subcommand | Example |
|---|---|
/garden add <instruction> | /garden add write a note about Nginx reverse proxy |
/garden edit <instruction> | /garden edit update the Nginx page to mention load balancing |
/garden delete <title> | /garden delete What is Nginx |
Backing tool: publish-garden.sh — the single script that handles privacy scan → commit → push → quartz build → nginx reload for all garden operations.
7. fitness-manager
| Field | Value |
|---|---|
| Hook | pre_gateway_dispatch |
| Commands | /fitness plan, /fitness actual, /fitness actual <date>, /fitness today |
| Purpose | Manage daily fitness journal — pull plans and log actual progress from chat |
How it works: Intercepts /fitness <plan|actual|today> messages and rewrites them as structured tasks ([FITNESS PLAN], [FITNESS ACTUAL], [FITNESS TODAY]) for the agent. The agent handles all file read/write, image attachment, and git commit/push logic via the fitness-journal skill.
Inline actuals support (today only): /fitness actual accepts inline details so you can log everything in one message:
/fitness actual 1. Lunch: nasi 2 centong... 2. Walk 20min... 3. Circuit 2 rounds...
Date-aware actuals (past dates): /fitness actual <date> <details> logs actuals for any past date using the full template — never the broken simplified format. The date parameter accepts:
| Input | Resolves to | Example |
|---|---|---|
yesterday | today - 1 | /fitness actual yesterday 1. Lunch... |
2026-07-18 | exact date | /fitness actual 2026-07-18 1. Lunch... |
2 days ago | today - 2 | /fitness actual 2 days ago 1. Lunch... |
Coach Tip Cascade: When logging actuals for a past date, the agent also updates the next day’s coach tip to reflect what happened — e.g., logging Saturday’s actuals cascades into Sunday’s tip saying “Based on your Saturday performance…”.
| Subcommand | Example |
|---|---|
/fitness plan | Shows today’s fitness plan from ~/obsidian-vault/Journal/ |
/fitness actual <details> | /fitness actual 1. Snack: apel 2. Dinner: nasi + ayam (today) |
/fitness actual <date> <details> | /fitness actual yesterday 1. Snack: apel 2. Dinner: nasi + ayam (past date) |
/fitness today | Shows full journal with plan + logged actuals |
/fitness help | Lists all available commands |
Backing skill: fitness-journal — codifies the full workflow: date parsing, template preservation (reads Fitness 2026-07-16.md as authoritative template), coach tip cascade, actuals update, image attachment, and commit & push.
8. journal-manager
| Field | Value |
|---|---|
| Hook | pre_gateway_dispatch |
| Commands | /journal <text> |
| Purpose | Write daily English journal entries with automatic grammar corrections and C1 upgrade |
How it works: Intercepts /journal <text> messages and rewrites them as a structured [JOURNAL] task for the agent. The agent saves the entry to ~/obsidian-vault/Journal/YYYY-MM-DD.md following the exact format from 2026-07-04.md:
- Frontmatter —
date, auto-generateddescription,tags: [journal, english-practice] - ## My Entry — the user’s original text preserved as-is
- ## Corrections 🛠️ — table with
\| Mistake \| Correction \|columns - ## C1 Upgrade 🚀 — blockquote with a full C1-level rewrite
- ## Key Takeaways 💡 — bullet points explaining vocabulary and grammar lessons
Then commits and pushes to the private vault.
| Usage | Example |
|---|---|
/journal <text> | /journal Today I felt like a regular day... |
Backing format reference: Journal/2026-07-04.md — the canonical template for all journal entries.
Bundled Plugins (shipped with Hermes)
These live in <hermes-repo>/plugins/ and are auto-loaded:
| Category | Plugins |
|---|---|
| Browser | browser |
| Image Generation | fal, krea, openai, openai-codex, openrouter, xai |
| Video Generation | fal, xai |
| Web Search | brave-free, ddgs, exa, firecrawl, parallel, searxng, tavily, xai |
| Memory | memory providers |
| Context Engine | context_engine |
| Platform | (messaging adapters) |
| Other | disk-cleanup, kanban, spotify, observability, security-guidance |
How to Install a New Plugin
Method 1: Manual Installation (Recommended)
Step 1 — Create the plugin directory:
mkdir -p ~/.hermes/plugins/my-plugin/Step 2 — Create plugin.yaml:
name: my-plugin
version: 1.0.0
description: "What my plugin does"
author: "Christian"
hooks:
- pre_gateway_dispatchStep 3 — Create __init__.py:
"""my-plugin — does something useful."""
import logging
logger = logging.getLogger(__name__)
def register(ctx) -> None:
"""Register hooks."""
ctx.register_hook("pre_gateway_dispatch", my_handler)
logger.info("my-plugin: registered (pre_gateway_dispatch)")
def my_handler(**kwargs) -> dict | None:
# Handle the event
return NoneStep 4 — Enable the plugin in config:
# Edit ~/.hermes/config.yaml
# Add to plugins.enabled list:
plugins:
enabled:
- model-switcher
- balance-check
- vps-status
- public-mode
- guest-manager
- garden-manager # <--- added
**Step 5 — Restart gateway:**
```bash
hermes gateway restart
Method 2: Using AI (Just Ask Jeri)
You can simply ask me (Jeri) and I’ll create the plugin, configure it, and restart the gateway for you. Example: “Jeri, create a plugin that sends me a reminder every hour.”
Configuration Reference
config.yaml (~/.hermes/config.yaml)
plugins:
enabled:
- model-switcher
- balance-check
- vps-status
- public-mode
- guest-manager
- garden-manager| Key | Type | Description |
|---|---|---|
plugins.enabled | list | List of plugin names to load (opt-in, not YAML string!) |
plugins.disabled | list | Explicit deny-list (overrides enabled) |
plugins.entries.<name> | dict | Per-plugin configuration (tool overrides, LLM access) |
⚠️ Important: plugins.enabled must be a proper YAML list (- item format), NOT a JSON string ('["item"]'). A JSON string will cause isinstance(enabled, list) to return False and no plugins will load.
.env (~/.hermes/.env)
WHATSAPP_ALLOWED_USERS=6285XXXXXXXXXX,6282XXXXXXXXXX| Variable | Description |
|---|---|
WHATSAPP_ALLOWED_USERS | Comma-separated phone numbers allowed to reach the bot. Required — without this, everyone is blocked at the bridge level. |
Lifecycle Hooks Reference
| Hook | When It Fires | Return |
|---|---|---|
pre_gateway_dispatch | Before a message is dispatched to the agent | {"action": "skip", "reason": "..."} — drop message |
{"action": "rewrite", "text": "..."} — replace message | ||
{"action": "allow"} or None — continue normally | ||
transform_llm_output | Before LLM response is sent to user | str — replace response |
None or "" — leave unchanged | ||
pre_tool_call | Before a tool executes | Modify or block the tool call |
post_tool_call | After a tool executes | Modify or annotate the result |
on_session_start | When a new session begins | Observer only |
on_session_end | When a session ends | Observer only |
kanban_task_claimed | When a kanban task is claimed | Observer only |
kanban_task_completed | When a kanban task completes | Observer only |
Troubleshooting
Plugin not loading?
Check agent.log:
grep "my-plugin" ~/.hermes/logs/agent.log“Plugin discovery complete: X found, Y enabled” — my plugin not counted?
- Check
plugin.yamlexists in the plugin directory - Check
plugins.enabledin config.yaml has the plugin name - Ensure
plugins.enabledis a YAML list, not a JSON string
Guest can access everything?
- Check plugin is in
plugins.enabled(config.yaml) - Check guest’s phone number is in the right list (
CHRISTIAN_IDSvsAPPROVED_GUEST_IDS) - Check bridge allowlist in
.env(WHATSAPP_ALLOWED_USERS) - Check if the WhatsApp bridge uses LID format — add
@lidvariants of the IDs
Plugin blocks everyone?
Check ID matching — the WhatsApp bridge may use LID format (123456789012345@lid) instead of phone numbers. Add both formats to your ID lists.
How to debug plugins:
export HERMES_PLUGINS_DEBUG=1
hermes gateway restart
# Now verbose plugin logs go to stderr in addition to agent.logLast updated: July 2026
Author: Jeri (Christian’s AI assistant)