🧩 Hermes Plugin System β€” Complete Guide

β€œThe core is a narrow waist; capability lives at the edges.” β€” Hermes Agent design philosophy


πŸ“‹ Table of Contents

  1. What Are Plugins?
  2. Plugin Architecture
  3. Installed Plugins
  4. How to Install a New Plugin
  5. Configuration Reference
  6. Lifecycle Hooks Reference
  7. Troubleshooting
  8. 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.enabled are loaded
  • Each plugin lives in its own directory with a plugin.yaml manifest + __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)

#SourcePathPurpose
1Bundled<repo>/plugins/<name>/Shipped with Hermes Agent (browser, image_gen, web, etc.)
2User~/.hermes/plugins/<name>/Custom plugins you create or install
3Project./.hermes/plugins/<name>/Project-specific plugins (opt-in via HERMES_ENABLE_PROJECT_PLUGINS)
4Pipvia hermes_agent.plugins entry pointPython 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 vars

init.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 hook
  • ctx.register_tool(...) β€” register a new tool
  • ctx.llm β€” access to the host-owned LLM
  • ctx.profile_name β€” current profile name

How Plugins Are Loaded

On every gateway startup, Hermes:

  1. Scans all 4 plugin sources for plugin.yaml files
  2. Parses manifests into PluginManifest objects
  3. Filters by plugins.enabled in config.yaml (only enabled plugins are loaded)
  4. Imports each enabled plugin’s __init__.py
  5. Calls register(ctx) which registers the plugin’s hooks/tools
  6. Logs success or errors in ~/.hermes/logs/agent.log

Installed Plugins

1. model-switcher

FieldValue
Hookpre_gateway_dispatch
Commands/gpt β†’ GPT models, /pro β†’ DeepSeek Pro, /flash β†’ back to Flash
PurposeSwitch 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

FieldValue
Hookpre_gateway_dispatch
Commands/balance
PurposeCheck 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

FieldValue
Hookpre_gateway_dispatch
Commands/status
PurposeCheck 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

FieldValue
Hookpre_gateway_dispatch + transform_llm_output
CommandsNone (authorization layer)
PurposeAllowlist-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 only

Note: The WhatsApp bridge uses LID format (e.g., 123456789012345@lid) rather than raw phone numbers. Both formats should be included.


5. guest-manager

FieldValue
Hookpre_gateway_dispatch
Commands/guests add, /guests remove, /guests list
PurposeManage 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

SubcommandExample
/guests add <number> <name> <role> [lid]/guests add 6282XXXXXXXXXX "Nama" guest
/guests remove <number>/guests remove 6282XXXXXXXXXX
/guests listShows 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

FieldValue
Hookpre_gateway_dispatch
Commands/garden add, /garden edit, /garden delete
PurposeManage 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.

SubcommandExample
/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:

CategoryPlugins
Browserbrowser
Image Generationfal, krea, openai, openai-codex, openrouter, xai
Video Generationfal, xai
Web Searchbrave-free, ddgs, exa, firecrawl, parallel, searxng, tavily, xai
Memorymemory providers
Context Enginecontext_engine
Platform(messaging adapters)
Otherdisk-cleanup, kanban, spotify, observability, security-guidance

How to Install a New Plugin

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_dispatch

Step 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 None

Step 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
KeyTypeDescription
plugins.enabledlistList of plugin names to load (opt-in, not YAML string!)
plugins.disabledlistExplicit deny-list (overrides enabled)
plugins.entries.<name>dictPer-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
VariableDescription
WHATSAPP_ALLOWED_USERSComma-separated phone numbers allowed to reach the bot. Required β€” without this, everyone is blocked at the bridge level.

Lifecycle Hooks Reference

HookWhen It FiresReturn
pre_gateway_dispatchBefore 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_outputBefore LLM response is sent to userstr β€” replace response
None or "" β€” leave unchanged
pre_tool_callBefore a tool executesModify or block the tool call
post_tool_callAfter a tool executesModify or annotate the result
on_session_startWhen a new session beginsObserver only
on_session_endWhen a session endsObserver only
kanban_task_claimedWhen a kanban task is claimedObserver only
kanban_task_completedWhen a kanban task completesObserver 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?

  1. Check plugin.yaml exists in the plugin directory
  2. Check plugins.enabled in config.yaml has the plugin name
  3. Ensure plugins.enabled is a YAML list, not a JSON string

Guest can access everything?

  1. Check plugin is in plugins.enabled (config.yaml)
  2. Check guest’s phone number is in the right list (CHRISTIAN_IDS vs APPROVED_GUEST_IDS)
  3. Check bridge allowlist in .env (WHATSAPP_ALLOWED_USERS)
  4. Check if the WhatsApp bridge uses LID format β€” add @lid variants 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.log


Last updated: July 2026
Author: Jeri (Christian’s AI assistant)