Event Hooks and the Plugin System
- Distinguish gateway hooks, plugin hooks, and shell hooks and choose the right one for a given use case
- Write a HOOK.yaml manifest and a handle(event_type, context) function for a gateway hook
- Explain why plugin hooks can block tool calls and inject context while gateway hooks cannot
- Configure a shell hook in config.yaml without writing any Python
- Identify the four plugin types and which are single-select vs multi-select
- Explain why plugins require explicit opt-in via plugins.enabled rather than auto-loading on discovery
Three Hook Systems, Not One
Hermes provides three complementary ways to react to things happening inside it, each suited to a different level of integration:
- Gateway hooks -- fire during messaging platform operations (Telegram, Discord, Slack, and so on)
- Plugin hooks -- run in both CLI and gateway contexts, registered via
ctx.register_hook() - Shell hooks -- execute any shell script for blocking, formatting, or context injection, no Python required
All three are non-blocking by design: errors inside a hook are caught and logged rather than crashing the agent. A hook that fails should degrade gracefully, not take down whatever it was attached to.
Gateway Hooks: Structure
A gateway hook lives in its own directory:
~/.hermes/hooks/
└── my-hook/
├── HOOK.yaml
└── handler.py
HOOK.yaml declares which events trigger it:
name: my-hook
description: Purpose of this hook
events:
- event_type_1
- event_type_2
handler.py defines a function literally named handle, which can be either synchronous or async:
async def handle(event_type: str, context: dict):
"""Process the event; can be async or regular def"""
Available Gateway Events
gateway:startupplatforms listsession:startplatform, user_id, session_idsession:endplatform, user_id, session_keyagent:startplatform, user_id, messageagent:stepplatform, iteration, tool_namesagent:endplatform, message, responsecommand:*platform, command, argsA practical use of agent:step: alert yourself on Telegram if a single agent turn reaches an unusually high iteration count, which often signals it is stuck in a loop rather than making real progress. A practical use of command:*: log every slash command usage across platforms to a single file for later review.
Plugin Hooks: More Power, More Trust
Plugin hooks, registered inside a plugin's register() function, can do something gateway hooks cannot: block a tool call outright or inject additional context into what the LLM sees, rather than just observing after the fact.
def register(ctx):
ctx.register_hook("pre_tool_call", my_function)
ctx.register_hook("post_tool_call", another_function)
Key plugin hook points: pre_tool_call, post_tool_call, pre_llm_call, post_llm_call, session lifecycle events, and transform hooks for tool, terminal, or LLM output.
Shell Hooks: No Python Required
Configured directly in config.yaml:
hooks:
pre_tool_call:
- matcher: "terminal"
command: "~/.hermes/agent-hooks/block-dangerous.sh"
timeout: 5
The script receives JSON via stdin and returns JSON via stdout -- any executable with a proper shebang line works, regardless of language. This is the lowest-friction option if you already have a working script and do not want to wrap it in a Python plugin just to attach it.
The Plugin System Proper
Beyond hooks, plugins can add entirely new tools, slash commands, and even alternative backends. There are four plugin types:
- General plugins -- tools, hooks, slash commands, CLI commands; multi-select, you can enable several at once
- Memory providers -- replace the built-in memory system; single-select, only one active
- Context engines -- replace context compression; single-select, only one active
- Model providers -- declare new inference backends; multi-register, with per-session selection
Plugin Directory Structure
~/.hermes/plugins/my-plugin/
├── plugin.yaml # manifest: name, version, description
├── __init__.py # register() function
├── schemas.py # tool schemas (LLM-visible definitions)
└── tools.py # tool handlers (execution logic)
Hermes discovers plugins from several sources: ~/.hermes/plugins/ (user-level), .hermes/plugins/ (project-local), bundled plugins shipped inside the Hermes repo itself, and pip entry points for installed packages.
What register(ctx) Can Do
ctx.register_tool()-- add a new callable tool with its schema and handlerctx.register_hook()-- attach a lifecycle callback, as shown abovectx.register_command()-- add a new in-chat slash commandctx.register_cli_command()-- add a newhermes <plugin>subcommandctx.register_skill()-- bundle skills underplugin:skillnamingctx.inject_message()-- queue a message directly into an active conversation
Plugins Are Opt-In, Not Auto-Loaded
General plugins and any user-installed backend plugin are disabled by default. Discovery finds them, but nothing actually loads until you explicitly add it to plugins.enabled in config.yaml. hermes plugins gives you interactive toggling, and hermes plugins install prompts you for enablement at install time rather than silently activating anything. The one exception is bundled infrastructure -- platform adapters and default backends that ship with Hermes itself -- which loads automatically since it is core, trusted functionality rather than a third-party extension.
The next lesson covers the security model that governs everything you have built across this entire track, and how to run it safely once it is acting without your direct supervision.
- Plugin hooks can block tool calls and inject context into what the LLM sees -- gateway hooks can only observe and react after the fact, a meaningful capability difference between the two systems
- Shell hooks require no Python -- any executable with a shebang that reads JSON from stdin and writes JSON to stdout works, the lowest-friction extension point in Hermes
- Memory providers and context engines are single-select (only one active) while general plugins and model providers are multi-select -- the architecture matches what genuinely makes sense to run more than one of at a time
- All hook systems are non-blocking by design -- errors are caught and logged, a failing hook should never crash the thing it is attached to
- Plugins are disabled by default even after discovery -- explicit opt-in via plugins.enabled (or hermes plugins) is required before any third-party plugin code actually runs, though bundled core infrastructure is exempt from this