π§© 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.
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)