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-level handle(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:

  1. Builds a handler list including exact event matches and wildcard listeners.
  2. Executes handlers sequentially, supporting both synchronous functions and async coroutines.
  3. 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 initialization
  • session:start – New session creation
  • session:reset – User executes /new or /reset
  • agent:start – Agent begins processing a message
  • agent:step – Each turn of the tool-calling loop
  • agent:end – Agent finishes processing
  • command:* – 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. Declaring command:* in your HOOK.yaml captures every slash command without enumerating them individually.
  • Async Compatibility – The registry handles both synchronous functions and async coroutines 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 a HOOK.yaml manifest plus a handler.py module.
  • The HookRegistry class in gateway/hooks.py handles discovery via discover_and_load() and emission via emit().
  • Supported events include gateway:startup, session:start, agent:step, and wildcard patterns like command:*.
  • 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →