Custom Plugins
11.7. Custom Plugins — The Deepest Extension Layer
Custom Plugins — the deepest extension layer. When MCP and the ready-made channels are not enough — you need a completely custom tool, want to intervene in the agent loop, or want to add a whole new gateway platform — this is the layer that makes it possible without forking Hermes.
Minimal structure
~/.hermes/plugins/hello-world/
├── plugin.yaml # manifest: name, description
└── __init__.py # register(ctx) — wires schema to handler
def register(ctx):
ctx.register_tool(
name="hello_world",
description="Respond to greetings",
handler=hello_world_handler,
schema={...}
)
ctx.register_hook("post_tool_call", log_every_call)
The four main APIs of PluginContext: register_tool (add a tool for the model to call), register_command (add a slash command shared by CLI + gateway), register_cli_command (add a hermes <plugin> ... subcommand), register_hook (run code at lifecycle points: before/after tool calls, before/after LLM calls, session start/end).
Opt-in safety by principle
This is the most important point to emphasize: a plugin does not run just because it is installed. Discovery finds it (it appears in hermes plugins list), but you must actively add it to plugins.enabled:
plugins:
enabled:
- hello-world
- disk-cleanup
disabled: # deny-list — always wins on name conflict with enabled
- noisy-plugin
When installing from GitHub, Hermes asks straight up “Enable ‘name’ now? [y/N]” — the default is no, forcing an explicit confirmation:
hermes plugins install owner/repo # asks before enabling
hermes plugins install owner/repo --enable # install + enable, for scripts
hermes plugins install owner/repo --ref <full-sha> # pin to one exact commit, reproducible
Verify before trusting
hermes plugins doctor owner/repo --ci
This command re-runs the entire discovery pipeline, manifest parsing, and hook/tool registration exactly as real Hermes does — reporting wrong hook names, callbacks missing **kwargs, or mismatches between declaration and actual registration. Important note: this is not a sandbox — plugin code still runs in-process with the current user’s rights and can spawn subprocesses — so only run Doctor (and enable plugins) for code you trust enough.
Not every extension needs Python
The documentation emphasizes an easily missed point: some extension surfaces deliberately use shell commands via config instead of Python — TTS, STT, shell hooks. Any existing CLI “becomes a plugin” with just a YAML declaration, no code to write:
hooks:
- event: post_tool_call
command: "notify-send 'Tool: {tool_name} done'"
when:
tools: [terminal, patch, write_file]
Field-tested lessons
- For simple needs (notifications, logging, calling a webhook when a tool finishes), always try a shell hook in config.yaml first — much faster than writing a full Python plugin.
- When installing third-party plugins for production, always pin with
--ref <full SHA>instead of branch/tag — to avoid the situation where plugin code changes silently after agit pullyou were not aware of. - Run
hermes plugins doctor --cias a mandatory step in your review process before enabling any community plugin — do not just read the code with your eyes and trust it.