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)
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) andcontext(dict) - Can be
async defor regulardef— 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
- On gateway startup,
HookRegistry.discover_and_load()scans~/.hermes/hooks/ - Each subdirectory with
HOOK.yaml+handler.pyis loaded dynamically - Handlers are registered for their declared events
- At each lifecycle point,
hooks.emit()fires all matching handlers - 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.