Hermes Agent Plugins: The Shape, the Register Entry Point, and How to Test Locally
How to author a Hermes plugin: the directory layout, the register(ctx) entry point, three real plugin examples on this build, and how to test a plugin locally before installing.

What a plugin actually does
A Hermes plugin extends the agent at one of the registered extension points: image_gen, model-provider, memory, platforms (gateway channels), context_engine, hooks, slash commands, or tools. Plugins are drop-in Python packages — no fork, no monkey-patch. The framework discovers them at startup, imports each one, and calls a single entry point: register(ctx). Everything the plugin contributes (a provider, a memory backend, a hook, a slash command, a tool) is handed to the ctx object inside that function. Drop the directory in, restart, and the surface appears.
The plugin system has four source locations, listed in priority order (later wins):
- Bundled —
<repo>/plugins/<name>/(ships with Hermes, used for built-in integrations likespotify,disk-cleanup,image_gen/*,memory/*). - User —
~/.hermes/plugins/<name>/(third-party and your own plugins). - Project —
./.hermes/plugins/<name>/(opt-in viaHERMES_ENABLE_PROJECT_PLUGINS). - Pip — any package exposing a
hermes_agent.pluginsentry point.
Each directory plugin must contain a plugin.yaml manifest and an __init__.py with a register(ctx) function. The scanner at hermes_cli/plugins.py enforces both.
Where plugins live
On this build (/opt/hermes), the bundled plugins/ directory looks like this:
plugins/
├── image_gen/ # image-generation backends (subdirs per backend)
│ ├── openai/ # kind: backend, requires_env: OPENAI_API_KEY
│ ├── openai-codex/
│ └── xai/
├── memory/ # long-term memory backends (subdirs per backend)
│ ├── hindsight/ # pip_dependencies, hooks: [on_session_end]
│ ├── byterover/
│ ├── honcho/
│ ├── mem0/
│ ├── supermemory/
│ ├── openviking/
│ ├── retaindb/
│ └── holographic/
├── model-providers/ # LLM / inference providers
│ ├── anthropic/ # kind: model-provider
│ ├── openai-codex/
│ ├── openrouter/
│ ├── ollama-cloud/
│ └── ... (24+ total)
├── platforms/ # gateway channels (chat front-ends)
│ ├── google_chat/
│ ├── irc/
│ └── teams/
├── observability/
│ └── langfuse/ # tracing backends
├── spotify/ # standalone backend plugin (flat layout)
├── disk-cleanup/ # standalone hooks + slash command plugin
├── google_meet/ # larger integration with audio bridge, bot, tools
├── kanban/ # dashboard + systemd layout (a different shape)
├── context_engine/ # context-compression engine plugin
└── strike-freedom-cockpit/ # example dashboard
The shape tells you what the plugin contributes: image_gen/, memory/, model-providers/, platforms/, observability/ are category subdirectories with one plugin per backend; spotify/, disk-cleanup/, google_meet/ are flat standalone plugins.
The canonical shape
Every plugin follows the same skeleton. Here’s a minimal ~/.hermes/plugins/hello_world/ plugin that registers one slash command and one hook:
mkdir -p ~/.hermes/plugins/hello_world
cd ~/.hermes/plugins/hello_world
plugin.yaml — manifest, what the scanner reads first:
name: hello_world
version: 1.0.0
description: "Trivial example — registers a /hello slash command and a post_tool_call hook."
author: You
kind: tool
provides_tools:
- hello
provides_hooks:
- post_tool_call
__init__.py — the entry point. Hermes imports this and calls register(ctx) once at startup:
"""hello_world plugin — minimal canonical example."""
from __future__ import annotations
import logging
from typing import Any, Dict
logger = logging.getLogger(__name__)
def _on_post_tool_call(*, tool_name: str, result: Any, **kwargs) -> None:
"""Hook handler — fires after every tool call the agent makes."""
if tool_name == "terminal":
logger.info("terminal just ran; result type=%s", type(result).__name__)
def _handle_hello(args: str = "") -> str:
"""Slash-command handler for /hello."""
return f"hello, {args or 'world'}!"
def register(ctx) -> None:
"""Plugin entry point — wire handlers into the context."""
ctx.register_hook("post_tool_call", _on_post_tool_call)
ctx.register_command(
name="hello",
handler=_handle_hello,
description="Print a friendly greeting.",
)
That’s the whole plugin. About 30 lines plus a 7-line manifest. Hermes picks it up the next time it boots; hermes tools will list hello; the hook will start firing on every tool call.
The ctx object exposes the registration surface — each extension point has its own register_* method. The most common ones:
| Surface | Method | Use for |
|---|---|---|
| Image-gen backend | ctx.register_image_gen_provider(provider) | DALL-E, Imagen, Flux wrappers |
| Model provider | register_provider(profile) | Anthropic, OpenAI, Ollama, custom inference |
| Memory backend | ctx.register_memory_provider(provider) | Hindsight, Mem0, Honcho |
| Tool | ctx.register_tool(name=..., schema=..., handler=..., check_fn=..., emoji=...) | Anything you want the LLM to be able to call |
| Hook | ctx.register_hook(event_name, handler) | React to post_tool_call, on_session_end, pre_tool_call, etc. |
| Slash command | ctx.register_command(name=..., handler=..., description=...) | Custom /your_command for the CLI/TUI |
| Gateway channel | platform-specific register_* | Discord, IRC, Teams adapters |
Three examples on this build today
Picking from /opt/hermes/plugins/, here are three real plugins that demonstrate the major shapes.
1. image_gen/openai — a backend provider. The whole register(ctx) is two real lines: import the class, call ctx.register_image_gen_provider(OpenAIImageGenProvider()). The 303-line __init__.py then implements the ImageGenProvider ABC, the three-tier model catalog, the async generate() call to OpenAI’s API, base64 decoding, and file caching. The plugin’s requires_env: [OPENAI_API_KEY] is what makes the loader gate installation behind the env var.
2. memory/hindsight — a memory backend with declarative hooks. The plugin.yaml gains two new manifest fields: pip_dependencies (loader installs these into the active venv during install) and hooks (declarative list of events the plugin subscribes to). The __init__.py registers a MemoryProvider subclass against the memory extension point — 1,747 lines, since it wraps a long-term memory system with a knowledge graph and embedded daemon lifecycle.
3. disk-cleanup — a flat standalone with hooks and a slash command. Smaller surface, no abstract base class. Demonstrates that the register(ctx) pattern is enough for plugins that don’t need to extend the model provider system, just add behavior.
Authoring checklist
- Manifest first. Write
plugin.yamlbefore any Python. Decide thename,version,description, and which fields apply (kind:,requires_env:,pip_dependencies:,provides_tools:,hooks:). The scanner rejects plugins without a manifest. - Verify dependencies install. If you list
pip_dependencies, runpip install -r <(yq '.pip_dependencies[]' plugin.yaml)before writing Python — find out the import paths you’ll need before you start coding. - Wire
register(ctx)to the right surface. Pick the extension point from the table above; pick the matchingctx.register_*method. A plugin can register multiple things in a singleregister(ctx)call. - Implement the abstract base. For typed surfaces (
image_gen,memory,model-providers), subclass the matching ABC inagent/(ImageGenProvider,MemoryProvider, etc.). The base classes define exactly what the framework will call on you. - Add a CLI smoke test. Even before you install, you can
importyour module and instantiate the provider by hand to catch schema errors and import-time mistakes.
Test locally before installing
Don’t install the plugin into ~/.hermes/plugins/ until you’ve smoke-tested it in isolation. From the repo root:
cd /opt/hermes
# 1. Confirm the module imports without errors.
# (Set any required env vars BEFORE the import — some plugins
# read them at module-load time.)
OPENAI_API_KEY=test-key-dummy-for-import-test \
python3 -c "from plugins.image_gen.openai import OpenAIImageGenProvider, register; \
print('register:', register); \
p = OpenAIImageGenProvider(); \
print('provider name:', p.name); \
print('default model:', p.default_model)"
The output on this build, just now:
register: <function register at 0x...>
provider name: openai
default model: gpt-image-2-medium
Three things just got proven: (a) all pip_dependencies are installed in the active venv, (b) the provider class is constructible with no arguments, (c) default_model resolves to the tier you expect. No agent restart, no hermes tools call, no risk of breaking the live build.
Then sanity-check the framework still loads with your plugin on the path:
python3 -c "
import hermes_cli.plugins as p
bundled = p.get_bundled_plugins_dir()
print('scanner sees:', sorted(d.name for d in bundled.iterdir() if d.is_dir()))
"
You should see your plugin’s directory in the listing. If it isn’t there, the manifest failed to parse — re-check plugin.yaml syntax and required fields.
Finally, before installing into ~/.hermes/plugins/, copy it to a scratch dir and exercise the actual code path that the hook/command will run. For disk-cleanup, that means calling _handle_slash("status") directly; for image_gen/openai, that means await OpenAIImageGenProvider().generate(...) with a real key.
What not to do
- Don’t skip
plugin.yaml— without a manifest, the loader won’t import the directory at all. Listpip_dependenciesandrequires_envhere; the install flow uses both. - Don’t put secrets in the plugin — declare them in
requires_env:and load at runtime. Never write secrets in__init__.py. - Don’t skip the abstract base class for typed surfaces (
ImageGenProvider,MemoryProvider,ProviderProfile) — those are the contracts the framework relies on. Duck-typed classes won’t show up inhermes toolsorhermes memory list.
Verification checklist
| Step | Command | What it proves |
|---|---|---|
| 1 | python3 -c "from plugins.<name> import register; print(register)" from /opt/hermes | Plugin loads without import errors and register is callable |
| 2 | python3 -c "import hermes_cli.plugins as p; print(p.get_bundled_plugins_dir())" | The plugin scanner sees the directory |
| 3 | Instantiate the provider/subclass directly and call its public method | The heavy-lifting code path runs end-to-end without the framework |
| 4 | Copy the plugin into ~/.hermes/plugins/, restart Hermes, run hermes tools | The plugin is wired into the live agent |
| 5 | Remove the plugin from ~/.hermes/plugins/, restart Hermes, run hermes tools | The plugin unregisters cleanly — nothing leaks across restarts |
Sources
- Hermes Agent plugins docs
- Build a Hermes Plugin (official guide, the calculator walkthrough)
- ABS companion: Hermes Agent Tools: Enabling Only What You Need
Last verified: 2026-07-14 against /opt/hermes/plugins/ on this install (Hermes v0.18.2 / 2026.7.7.2).


Submit a take
Have a different read on this? Drop a comment below — your email isn't published, and I read every one. Nothing leaves the site until I approve it.