Files
hermes-agent/website/docs/user-guide/features/hooks.md
Teknium ef47531617 docs: unify hooks documentation — add plugin hooks to hooks page, add session:end event
The hooks page only documented gateway event hooks (HOOK.yaml system).
The plugins page listed plugin hooks (pre_tool_call, etc.) that weren't
referenced from the hooks page, which was confusing.

Changes:
- hooks.md: Add overview table showing both hook systems
- hooks.md: Add Plugin Hooks section with available hooks, callback
  signatures, and example
- hooks.md: Add missing session:end gateway event (emitted but undocumented)
- hooks.md: Mark pre_llm_call, post_llm_call, on_session_start,
  on_session_end as planned (defined in VALID_HOOKS but not yet invoked)
- hooks.md: Update info box to cross-reference plugin hooks
- hooks.md: Fix heading hierarchy (gateway content as subsections)
- plugins.md: Add cross-reference to hooks page for full details
- plugins.md: Mark planned hooks as (planned)
2026-03-24 18:48:47 -07:00

7.6 KiB

sidebar_position, title, description
sidebar_position title description
6 Event Hooks Run custom code at key lifecycle points — log activity, send alerts, post to webhooks

Event Hooks

Hermes has two hook systems that run custom code at key lifecycle points:

System Registered via Runs in Use case
Gateway hooks HOOK.yaml + handler.py in ~/.hermes/hooks/ Gateway only Logging, alerts, webhooks
Plugin hooks ctx.register_hook() in a plugin CLI + Gateway Tool interception, metrics, guardrails

Both systems are non-blocking — errors in any hook are caught and logged, never crashing the agent.

Gateway Event Hooks

Gateway hooks fire automatically during gateway operation (Telegram, Discord, Slack, WhatsApp) without blocking the main agent pipeline.

Creating a Hook

Each hook is a directory under ~/.hermes/hooks/ containing two files:

~/.hermes/hooks/
└── my-hook/
    ├── HOOK.yaml      # Declares which events to listen for
    └── handler.py     # Python handler function

HOOK.yaml

name: my-hook
description: Log all agent activity to a file
events:
  - agent:start
  - agent:end
  - agent:step

The events list determines which events trigger your handler. You can subscribe to any combination of events, including wildcards like command:*.

handler.py

import json
from datetime import datetime
from pathlib import Path

LOG_FILE = Path.home() / ".hermes" / "hooks" / "my-hook" / "activity.log"

async def handle(event_type: str, context: dict):
    """Called for each subscribed event. Must be named 'handle'."""
    entry = {
        "timestamp": datetime.now().isoformat(),
        "event": event_type,
        **context,
    }
    with open(LOG_FILE, "a") as f:
        f.write(json.dumps(entry) + "\n")

Handler rules:

  • Must be named handle
  • Receives event_type (string) and context (dict)
  • Can be async def or regular def — both work
  • Errors are caught and logged, never crashing the agent

Available Events

Event When it fires Context keys
gateway:startup Gateway process starts platforms (list of active platform names)
session:start New messaging session created platform, user_id, session_id, session_key
session:end Session ended (before reset) platform, user_id, session_key
session:reset User ran /new or /reset platform, user_id, session_key
agent:start Agent begins processing a message platform, user_id, session_id, message
agent:step Each iteration of the tool-calling loop platform, user_id, session_id, iteration, tool_names
agent:end Agent finishes processing platform, user_id, session_id, message, response
command:* Any slash command executed platform, user_id, command, args

Wildcard Matching

Handlers registered for command:* fire for any command: event (command:model, command:reset, etc.). Monitor all slash commands with a single subscription.

Examples

Telegram Alert on Long Tasks

Send yourself a message when the agent takes more than 10 steps:

# ~/.hermes/hooks/long-task-alert/HOOK.yaml
name: long-task-alert
description: Alert when agent is taking many steps
events:
  - agent:step
# ~/.hermes/hooks/long-task-alert/handler.py
import os
import httpx

THRESHOLD = 10
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
CHAT_ID = os.getenv("TELEGRAM_HOME_CHANNEL")

async def handle(event_type: str, context: dict):
    iteration = context.get("iteration", 0)
    if iteration == THRESHOLD and BOT_TOKEN and CHAT_ID:
        tools = ", ".join(context.get("tool_names", []))
        text = f"⚠️ Agent has been running for {iteration} steps. Last tools: {tools}"
        async with httpx.AsyncClient() as client:
            await client.post(
                f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
                json={"chat_id": CHAT_ID, "text": text},
            )

Command Usage Logger

Track which slash commands are used:

# ~/.hermes/hooks/command-logger/HOOK.yaml
name: command-logger
description: Log slash command usage
events:
  - command:*
# ~/.hermes/hooks/command-logger/handler.py
import json
from datetime import datetime
from pathlib import Path

LOG = Path.home() / ".hermes" / "logs" / "command_usage.jsonl"

def handle(event_type: str, context: dict):
    LOG.parent.mkdir(parents=True, exist_ok=True)
    entry = {
        "ts": datetime.now().isoformat(),
        "command": context.get("command"),
        "args": context.get("args"),
        "platform": context.get("platform"),
        "user": context.get("user_id"),
    }
    with open(LOG, "a") as f:
        f.write(json.dumps(entry) + "\n")

Session Start Webhook

POST to an external service on new sessions:

# ~/.hermes/hooks/session-webhook/HOOK.yaml
name: session-webhook
description: Notify external service on new sessions
events:
  - session:start
  - session:reset
# ~/.hermes/hooks/session-webhook/handler.py
import httpx

WEBHOOK_URL = "https://your-service.example.com/hermes-events"

async def handle(event_type: str, context: dict):
    async with httpx.AsyncClient() as client:
        await client.post(WEBHOOK_URL, json={
            "event": event_type,
            **context,
        }, timeout=5)

How It Works

  1. On gateway startup, HookRegistry.discover_and_load() scans ~/.hermes/hooks/
  2. Each subdirectory with HOOK.yaml + handler.py is loaded dynamically
  3. Handlers are registered for their declared events
  4. At each lifecycle point, hooks.emit() fires all matching handlers
  5. Errors in any handler are caught and logged — a broken hook never crashes the agent

:::info Gateway hooks only fire in the gateway (Telegram, Discord, Slack, WhatsApp). The CLI does not load gateway hooks. For hooks that work everywhere, use plugin hooks. :::

Plugin Hooks

Plugins can register hooks that fire in both CLI and gateway sessions. These are registered programmatically via ctx.register_hook() in your plugin's register() function.

def register(ctx):
    ctx.register_hook("pre_tool_call", my_callback)
    ctx.register_hook("post_tool_call", my_callback)

Available Plugin Hooks

Hook Fires when Callback receives
pre_tool_call Before any tool executes tool_name, args, task_id
post_tool_call After any tool returns tool_name, args, result, task_id
pre_llm_call Before LLM API request (planned — not yet wired)
post_llm_call After LLM API response (planned — not yet wired)
on_session_start Session begins (planned — not yet wired)
on_session_end Session ends (planned — not yet wired)

Callbacks receive keyword arguments matching the columns above:

def my_callback(**kwargs):
    tool = kwargs["tool_name"]
    args = kwargs["args"]
    # ...

Example: Block Dangerous Tools

# ~/.hermes/plugins/tool-guard/__init__.py
BLOCKED = {"terminal", "write_file"}

def guard(**kwargs):
    if kwargs["tool_name"] in BLOCKED:
        print(f"⚠ Blocked tool call: {kwargs['tool_name']}")

def register(ctx):
    ctx.register_hook("pre_tool_call", guard)

See the Plugins guide for full details on creating plugins.