The gateway now ships with a built-in boot-md hook that checks for ~/.hermes/BOOT.md on every startup. If the file exists, the agent executes its instructions in a background thread. No installation or configuration needed — just create the file. No BOOT.md = zero overhead (the hook silently returns). Implementation: - gateway/builtin_hooks/boot_md.py: handler with boot prompt, background thread, [SILENT] suppression, error handling - gateway/hooks.py: _register_builtin_hooks() called at the start of discover_and_load() to wire in built-in hooks - Docs updated: hooks page documents BOOT.md as a built-in feature
8.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
Boot Checklist (BOOT.md) — Built-in
The gateway ships with a built-in boot-md hook that looks for ~/.hermes/BOOT.md on every startup. If the file exists, the agent runs its instructions in a background session. No installation needed — just create the file.
Create ~/.hermes/BOOT.md:
# Startup Checklist
1. Check if any cron jobs failed overnight — run `hermes cron list`
2. Send a message to Discord #general saying "Gateway restarted, all systems go"
3. Check if /opt/app/deploy.log has any errors from the last 24 hours
The agent runs these instructions in a background thread so it doesn't block gateway startup. If nothing needs attention, the agent replies with [SILENT] and no message is delivered.
:::tip No BOOT.md? The hook silently skips — zero overhead. Create the file whenever you need startup automation, delete it when you don't. :::
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 | 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.