Skip to main content

Event Hooks

The hooks system lets you run custom code at key points in the agent lifecycle — session creation, slash commands, each tool-calling step, and more. Hooks fire automatically during gateway operation 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

EventWhen it firesContext keys
gateway:startupGateway process startsplatforms (list of active platform names)
session:startNew messaging session createdplatform, user_id, session_id, session_key
session:resetUser ran /new or /resetplatform, user_id, session_key
agent:startAgent begins processing a messageplatform, user_id, session_id, message
agent:stepEach iteration of the tool-calling loopplatform, user_id, session_id, iteration, tool_names
agent:endAgent finishes processingplatform, user_id, session_id, message, response
command:*Any slash command executedplatform, 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

Hooks only fire in the gateway (Telegram, Discord, Slack, WhatsApp). The CLI does not currently load hooks.