Understanding the Event Hooks System in Hermes Agent: A Complete Guide
The event hooks system in Hermes Agent is a lightweight Python subsystem that executes custom code at specific lifecycle moments, allowing you to extend functionality by placing a HOOK.yaml manifest and handler.py file in ~/.hermes/hooks/.
Hermes Agent, developed by NousResearch, ships with a tiny but powerful event hooks system that lets developers inject arbitrary Python logic into the gateway and agent lifecycles. Whether you need to log tool calls to an external service, monitor slash commands, or initialize resources on startup, this system provides a safe, isolated mechanism without modifying core source code.
How the Event Hooks System in Hermes Agent Works
The architecture centers on a single registry class that discovers user-provided modules and emits events asynchronously. All core logic resides in gateway/hooks.py.
HookRegistry and Discovery
The HookRegistry class handles the entire lifecycle of hook management. During gateway startup, the registry calls discover_and_load(), which scans the directory defined by HOOKS_DIR (hardcoded to Path(os.path.expanduser("~/.hermes/hooks")) at line 30 of gateway/hooks.py).
For each subdirectory in ~/.hermes/hooks/, the loader expects two files:
HOOK.yaml– A manifest declaring the hook name, description, and the list of events to subscribe to.handler.py– A Python module containing a top-levelhandle(event_type, context)callable.
Valid handlers are cached in the _handlers dictionary keyed by event names, supporting both exact matches and wildcard patterns like command:*.
Event Emission
When significant lifecycle moments occur, the gateway calls await hooks.emit(event_type, context) (implemented at lines 118-150 in gateway/hooks.py). This method:
- Builds a handler list including exact event matches and wildcard listeners.
- Executes handlers sequentially, supporting both synchronous functions and
asynccoroutines. - Catches and logs all exceptions with a
[hooks]prefix, ensuring faulty hooks never crash the main pipeline.
The system supports eight distinct event types declared in gateway/hooks.py (lines 9-16):
gateway:startup– Gateway process initializationsession:start– New session creationsession:reset– User executes/newor/resetagent:start– Agent begins processing a messageagent:step– Each turn of the tool-calling loopagent:end– Agent finishes processingcommand:*– Wildcard matching any slash command
Creating Custom Hooks in ~/.hermes/hooks/
Building a custom hook requires three components: a directory, a manifest, and a handler module.
Step 1: Create the Hook Directory
Create a new subdirectory under ~/.hermes/hooks/ using a descriptive name for your extension. The directory name becomes the hook identifier.
mkdir -p ~/.hermes/hooks/my-logger
Step 2: Define the HOOK.yaml Manifest
Inside your hook directory, create a HOOK.yaml file declaring the metadata and event subscriptions. The name and events keys are mandatory.
# ~/.hermes/hooks/my-logger/HOOK.yaml
name: my-logger
description: Log every tool-call step to a remote service
events:
- agent:step
You can subscribe to multiple events or use wildcards like command:* to capture all slash commands.
Step 3: Implement the handler.py File
Create a handler.py file exporting a handle(event_type, context) function. This function receives the event name as a string and a context dictionary containing event-specific data. The function can be synchronous or asynchronous.
# ~/.hermes/hooks/my-logger/handler.py
import json
import httpx
async def handle(event_type, context):
"""Post agent step details to an external monitoring endpoint."""
payload = {
"event": event_type,
"session": context.get("session_id"),
"tool": context.get("tool_name"),
"iteration": context.get("iteration"),
}
async with httpx.AsyncClient() as client:
await client.post("https://example.com/agent-logs", json=payload)
For agent:step events, the context dictionary includes keys like task_id, tool_name, and iteration. For session:start, it includes session_id and config.
Step 4: Reload and Activate
The HookRegistry discovers hooks only during the discovery phase, typically at gateway startup. To activate your new hook:
hermes gateway restart
Alternatively, if running a development instance, you can trigger discover_and_load() programmatically, though a full restart is the standard workflow for production deployments.
Practical Examples of Custom Event Hooks
Below are three complete, runnable examples demonstrating different hook patterns.
Example 1: Minimal Session Greeting
This hook prints a console message whenever a new session begins.
# ~/.hermes/hooks/hello/HOOK.yaml
name: hello
description: Greet when a new session begins
events:
- session:start
# ~/.hermes/hooks/hello/handler.py
def handle(event_type, context):
print("[hooks] New session started – welcome!")
Example 2: Local Tool-Call Logger
This synchronous hook appends every tool invocation to a local log file.
# ~/.hermes/hooks/tool-recorder/HOOK.yaml
name: tool-recorder
description: Append tool usage to ~/.hermes/tool.log
events:
- agent:step
# ~/.hermes/hooks/tool-recorder/handler.py
import os
from datetime import datetime
def handle(event_type, context):
log_path = os.path.expanduser("~/.hermes/tool.log")
entry = f"{datetime.utcnow().isoformat()} | {context.get('tool_name')} | {context.get('iteration')}\n"
with open(log_path, "a", encoding="utf-8") as f:
f.write(entry)
Example 3: Asynchronous Command Monitor
This async hook forwards all slash commands to a monitoring service using HTTP.
# ~/.hermes/hooks/command-watcher/HOOK.yaml
name: command-watcher
description: Forward all slash commands to a monitoring service
events:
- command:*
# ~/.hermes/hooks/command-watcher/handler.py
import httpx
async def handle(event_type, context):
async with httpx.AsyncClient() as client:
await client.post(
"https://monitor.example.com/command",
json={"event": event_type, "data": context},
)
Safety Guarantees and Error Handling
The event hooks system in Hermes Agent prioritizes stability through three core mechanisms:
- Error Isolation – Every hook executes inside a try-except block. If a handler raises an exception, the registry logs it with a
[hooks]prefix and continues processing remaining hooks. Faulty code never crashes the gateway or aborts the agent pipeline. - Wildcard Support – The
emit()method builds handler lists using both exact string matches and glob-style wildcards. Declaringcommand:*in yourHOOK.yamlcaptures every slash command without enumerating them individually. - Async Compatibility – The registry handles both synchronous functions and
asynccoroutines transparently.emit()awaits async handlers but never blocks the event loop for synchronous ones.
Summary
The event hooks system in Hermes Agent provides a robust extension point for the NousResearch gateway. Key takeaways include:
- Hooks reside in
~/.hermes/hooks/and require aHOOK.yamlmanifest plus ahandler.pymodule. - The
HookRegistryclass ingateway/hooks.pyhandles discovery viadiscover_and_load()and emission viaemit(). - Supported events include
gateway:startup,session:start,agent:step, and wildcard patterns likecommand:*. - Handlers receive
(event_type, context)and may be synchronous or asynchronous. - Errors are isolated and logged, ensuring hook failures never disrupt core operations.
Frequently Asked Questions
What file structure is required for a custom hook in Hermes Agent?
You must create a directory under ~/.hermes/hooks/ containing exactly two files: a HOOK.yaml manifest describing the hook name and subscribed events, and a handler.py Python module exporting a handle(event_type, context) function. The directory name typically matches the hook name for clarity.
Can I use async functions in my hook handlers?
Yes. The HookRegistry in gateway/hooks.py automatically detects whether your handle function is a coroutine. If it is async, the registry awaits it during emit(); if synchronous, it calls it directly. Both styles can coexist in the same hook directory.
What happens if my custom hook raises an exception?
The event hooks system catches all exceptions inside a try-except block during handler execution. It logs the traceback with a [hooks] prefix to stderr or the configured logger, then continues with the next handler. Your hook failure never propagates to the gateway or interrupts the agent session.
How do I listen to every slash command without listing them individually?
Use the wildcard event pattern command:* in your HOOK.yaml events list. The emit() method in gateway/hooks.py expands wildcards when building the handler list, so a hook subscribed to command:* receives events for /new, /reset, and any other slash command automatically.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →