guide · computers

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.

July 14, 2026 · By Alastair Fraser

Friendly retro robot plugging a labeled module-shaped crate into a tall shelf of plugin crates, each crate tagged with a tiny icon (image, message, model, hook).

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):

  1. Bundled<repo>/plugins/<name>/ (ships with Hermes, used for built-in integrations like spotify, disk-cleanup, image_gen/*, memory/*).
  2. User~/.hermes/plugins/<name>/ (third-party and your own plugins).
  3. Project./.hermes/plugins/<name>/ (opt-in via HERMES_ENABLE_PROJECT_PLUGINS).
  4. Pip — any package exposing a hermes_agent.plugins entry 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:

SurfaceMethodUse for
Image-gen backendctx.register_image_gen_provider(provider)DALL-E, Imagen, Flux wrappers
Model providerregister_provider(profile)Anthropic, OpenAI, Ollama, custom inference
Memory backendctx.register_memory_provider(provider)Hindsight, Mem0, Honcho
Toolctx.register_tool(name=..., schema=..., handler=..., check_fn=..., emoji=...)Anything you want the LLM to be able to call
Hookctx.register_hook(event_name, handler)React to post_tool_call, on_session_end, pre_tool_call, etc.
Slash commandctx.register_command(name=..., handler=..., description=...)Custom /your_command for the CLI/TUI
Gateway channelplatform-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

  1. Manifest first. Write plugin.yaml before any Python. Decide the name, version, description, and which fields apply (kind:, requires_env:, pip_dependencies:, provides_tools:, hooks:). The scanner rejects plugins without a manifest.
  2. Verify dependencies install. If you list pip_dependencies, run pip install -r <(yq '.pip_dependencies[]' plugin.yaml) before writing Python — find out the import paths you’ll need before you start coding.
  3. Wire register(ctx) to the right surface. Pick the extension point from the table above; pick the matching ctx.register_* method. A plugin can register multiple things in a single register(ctx) call.
  4. Implement the abstract base. For typed surfaces (image_gen, memory, model-providers), subclass the matching ABC in agent/ (ImageGenProvider, MemoryProvider, etc.). The base classes define exactly what the framework will call on you.
  5. Add a CLI smoke test. Even before you install, you can import your 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. List pip_dependencies and requires_env here; 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 in hermes tools or hermes memory list.

Verification checklist

StepCommandWhat it proves
1python3 -c "from plugins.<name> import register; print(register)" from /opt/hermesPlugin loads without import errors and register is callable
2python3 -c "import hermes_cli.plugins as p; print(p.get_bundled_plugins_dir())"The plugin scanner sees the directory
3Instantiate the provider/subclass directly and call its public methodThe heavy-lifting code path runs end-to-end without the framework
4Copy the plugin into ~/.hermes/plugins/, restart Hermes, run hermes toolsThe plugin is wired into the live agent
5Remove the plugin from ~/.hermes/plugins/, restart Hermes, run hermes toolsThe plugin unregisters cleanly — nothing leaks across restarts

Sources

Last verified: 2026-07-14 against /opt/hermes/plugins/ on this install (Hermes v0.18.2 / 2026.7.7.2).

Sources

#hermes#plugins#extending#python#author-how-to

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.

Your email address will not be published. Required fields are marked.