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).
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) 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 | 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.