sidebar_position: 11 sidebar_label: “Plugins” title: “Plugins” description: “Extend Hermes with custom tools, hooks, and integrations via the plugin system”
Plugins
Hermes has a plugin system for adding custom tools, hooks, and integrations without modifying core code.
→ Build a Hermes Plugin — step-by-step guide with a complete working example.
Quick overview
Drop a directory into ~/.hermes/plugins/ with a plugin.yaml and Python code:
~/.hermes/plugins/my-plugin/
├── plugin.yaml # manifest
├── __init__.py # register() — wires schemas to handlers
├── schemas.py # tool schemas (what the LLM sees)
└── tools.py # tool handlers (what runs when called)
Start Hermes — your tools appear alongside built-in tools. The model can call them immediately.
Minimal working example
Here is a complete plugin that adds a hello_world tool and logs every tool call via a hook.
~/.hermes/plugins/hello-world/plugin.yaml
name: hello-world
version: "1.0"
description: A minimal example plugin
~/.hermes/plugins/hello-world/__init__.py
"""Minimal Hermes plugin — registers a tool and a hook."""
def register(ctx):
# --- Tool: hello_world ---
schema = {
"name": "hello_world",
"description": "Returns a friendly greeting for the given name.",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name to greet",
}
},
"required": ["name"],
},
}
def handle_hello(params):
name = params.get("name", "World")
return f"Hello, {name}! 👋 (from the hello-world plugin)"
ctx.register_tool("hello_world", schema, handle_hello)
# --- Hook: log every tool call ---
def on_tool_call(tool_name, params, result):
print(f"[hello-world] tool called: {tool_name}")
ctx.register_hook("post_tool_call", on_tool_call)
Drop both files into ~/.hermes/plugins/hello-world/, restart Hermes, and the model can immediately call hello_world. The hook prints a log line after every tool invocation.
Project-local plugins under ./.hermes/plugins/ are disabled by default. Enable them only for trusted repositories by setting HERMES_ENABLE_PROJECT_PLUGINS=true before starting Hermes.
What plugins can do
| Capability | How |
|---|---|
| Add tools | ctx.register_tool(name, schema, handler) |
| Add hooks | ctx.register_hook("post_tool_call", callback) |
| Add CLI commands | ctx.register_cli_command(name, help, setup_fn, handler_fn) — adds hermes <plugin> <subcommand> |
| Inject messages | ctx.inject_message(content, role="user") — see Injecting Messages |
| Ship data files | Path(__file__).parent / "data" / "file.yaml" |
| Bundle skills | Copy skill.md to ~/.hermes/skills/ at load time |
| Gate on env vars | requires_env: [API_KEY] in plugin.yaml — prompted during hermes plugins install |
| Distribute via pip | [project.entry-points."hermes_agent.plugins"] |
Plugin discovery
| Source | Path | Use case |
|---|---|---|
| User | ~/.hermes/plugins/ | Personal plugins |
| Project | .hermes/plugins/ | Project-specific plugins (requires HERMES_ENABLE_PROJECT_PLUGINS=true) |
| pip | hermes_agent.plugins entry_points | Distributed packages |
Available hooks
Plugins can register callbacks for these lifecycle events. See the Event Hooks page for full details, callback signatures, and examples.
| Hook | Fires when |
|---|---|
pre_tool_call | Before any tool executes |
post_tool_call | After any tool returns |
pre_llm_call | Once per turn, before the LLM loop — can return {"context": "..."} to inject context into the user message |
post_llm_call | Once per turn, after the LLM loop (successful turns only) |
on_session_start | New session created (first turn only) |
on_session_end | End of every run_conversation call + CLI exit handler |
Plugin types
Hermes has three kinds of plugins:
| Type | What it does | Selection | Location |
|---|---|---|---|
| General plugins | Add tools, hooks, CLI commands | Multi-select (enable/disable) | ~/.hermes/plugins/ |
| Memory providers | Replace or augment built-in memory | Single-select (one active) | plugins/memory/ |
| Context engines | Replace the built-in context compressor | Single-select (one active) | plugins/context_engine/ |
Memory providers and context engines are provider plugins — only one of each type can be active at a time. General plugins can be enabled in any combination.
Managing plugins
hermes plugins # unified interactive UI
hermes plugins list # table view with enabled/disabled status
hermes plugins install user/repo # install from Git
hermes plugins update my-plugin # pull latest
hermes plugins remove my-plugin # uninstall
hermes plugins enable my-plugin # re-enable a disabled plugin
hermes plugins disable my-plugin # disable without removing
Interactive UI
Running hermes plugins with no arguments opens a composite interactive screen:
Plugins
↑↓ navigate SPACE toggle ENTER configure/confirm ESC done
General Plugins
→ [✓] my-tool-plugin — Custom search tool
[ ] webhook-notifier — Event hooks
Provider Plugins
Memory Provider ▸ honcho
Context Engine ▸ compressor
- General Plugins section — checkboxes, toggle with SPACE
- Provider Plugins section — shows current selection. Press ENTER to drill into a radio picker where you choose one active provider.
Provider plugin selections are saved to config.yaml:
memory:
provider: "honcho" # empty string = built-in only
context:
engine: "compressor" # default built-in compressor
Disabling general plugins
Disabled plugins remain installed but are skipped during loading. The disabled list is stored in config.yaml under plugins.disabled:
plugins:
disabled:
- my-noisy-plugin
In a running session, /plugins shows which plugins are currently loaded.
Injecting Messages
Plugins can inject messages into the active conversation using ctx.inject_message():
ctx.inject_message("New data arrived from the webhook", role="user")
Signature: ctx.inject_message(content: str, role: str = "user") -> bool
How it works:
- If the agent is idle (waiting for user input), the message is queued as the next input and starts a new turn.
- If the agent is mid-turn (actively running), the message interrupts the current operation — the same as a user typing a new message and pressing Enter.
- For non-
"user"roles, the content is prefixed with[role](e.g.[system] ...). - Returns
Trueif the message was queued successfully,Falseif no CLI reference is available (e.g. in gateway mode).
- This enables plugins like remote control viewers, messaging bridges, or webhook receivers to feed messages into the conversation from external sources.
-
::note
inject_messageis only available in CLI mode. In gateway mode, there is no CLI reference and the method returnsFalse. -
::
See the full guide for handler contracts, schema format, hook behavior, error handling, and common mistakes.