Files
hermes-agent/website/docs/user-guide/features/hooks.md
Teknium 02fb7c4aaf docs: comprehensive docs audit — fix 12 stale/missing items across 10 pages (#3618)
Fixes found by auditing docs against recent PRs/commits:

Critical (misleading):
- hooks.md: Remove stale 'planned — not yet wired' markers for 4 hooks
  that are now active (#3542). Add correct callback signatures.
- security.md: Update tirith verdict behavior — block verdicts now go
  through approval flow instead of hard-blocking (#3428). Add pkill/killall
  self-termination guard and gateway-run backgrounding patterns (#3593).

New feature docs:
- configuration.md: Add tool_use_enforcement section with value table
  (auto/true/false/list) from #3551/#3528.
- configuration.md: Expand auxiliary config with per-task timeouts
  (compression 120s, web_extract 30s, approval 30s) from #3597.
- api-server.md: Add /v1/health alias, Security Headers section,
  CORS details (Max-Age, SSE headers, Idempotency-Key) from
  #3572/#3573/#3576/#3580/#3530.

Stale/incomplete:
- configuration.md: Fix Alibaba model name qwen-plus -> qwen3.5-plus (#3484).
- environment-variables.md: Specify actual DashScope default URL.
- cli-commands.md: Add alibaba to --provider list.
- fallback-providers.md: Add Alibaba/DashScope to provider table.
- email.md: Document noreply/automated sender filtering (#3606).
- toolsets-reference.md: Add 4 missing platform toolsets — matrix,
  mattermost, dingtalk, api-server (#3583).
- skills.md: List default GitHub taps including garrytan/gstack (#3605).
2026-03-28 15:26:35 -07:00

7.7 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 session_id, user_message, conversation_history, is_first_turn, model, platform
post_llm_call After LLM API response session_id, user_message, assistant_response, conversation_history, model, platform
on_session_start Session begins session_id, model, platform
on_session_end Session ends session_id, completed, interrupted, model, platform

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.