2026-03-05 05:24:55 -08:00
---
sidebar_position: 6
title: "Event Hooks"
description: "Run custom code at key lifecycle points — log activity, send alerts, post to webhooks"
---
# Event Hooks
2026-03-24 18:34:14 -07:00
Hermes has two hook systems that run custom code at key lifecycle points:
2026-03-05 05:24:55 -08:00
2026-03-24 18:34:14 -07:00
| System | Registered via | Runs in | Use case |
|--------|---------------|---------|----------|
| * * [Gateway hooks ](#gateway-event-hooks )** | `HOOK.yaml` + `handler.py` in `~/.hermes/hooks/` | Gateway only | Logging, alerts, webhooks |
| * * [Plugin hooks ](#plugin-hooks )** | `ctx.register_hook()` in a [plugin ](/docs/user-guide/features/plugins ) | 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
2026-03-05 05:24:55 -08:00
Each hook is a directory under `~/.hermes/hooks/` containing two files:
2026-03-14 22:49:57 -07:00
```text
2026-03-05 05:24:55 -08:00
~/.hermes/hooks/
└── my-hook/
├── HOOK.yaml # Declares which events to listen for
└── handler.py # Python handler function
```
2026-03-24 18:34:14 -07:00
#### HOOK.yaml
2026-03-05 05:24:55 -08:00
```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:*` .
2026-03-24 18:34:14 -07:00
#### handler.py
2026-03-05 05:24:55 -08:00
```python
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
2026-03-24 18:34:14 -07:00
### Available Events
2026-03-05 05:24:55 -08:00
| 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` |
2026-03-24 18:34:14 -07:00
| `session:end` | Session ended (before reset) | `platform` , `user_id` , `session_key` |
2026-03-05 05:24:55 -08:00
| `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` |
2026-03-24 18:34:14 -07:00
#### Wildcard Matching
2026-03-05 05:24:55 -08:00
Handlers registered for `command:*` fire for any `command:` event (`command:model` , `command:reset` , etc.). Monitor all slash commands with a single subscription.
2026-03-24 18:34:14 -07:00
### Examples
2026-03-05 05:24:55 -08:00
2026-03-29 10:19:54 -07:00
#### 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` :**
```markdown
# 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.
:::
2026-03-24 18:34:14 -07:00
#### Telegram Alert on Long Tasks
2026-03-05 05:24:55 -08:00
Send yourself a message when the agent takes more than 10 steps:
```yaml
# ~/.hermes/hooks/long-task-alert/HOOK.yaml
name: long-task-alert
description: Alert when agent is taking many steps
events:
- agent:step
```
```python
# ~/.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},
)
```
2026-03-24 18:34:14 -07:00
#### Command Usage Logger
2026-03-05 05:24:55 -08:00
Track which slash commands are used:
```yaml
# ~/.hermes/hooks/command-logger/HOOK.yaml
name: command-logger
description: Log slash command usage
events:
- command:*
```
```python
# ~/.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")
```
2026-03-24 18:34:14 -07:00
#### Session Start Webhook
2026-03-05 05:24:55 -08:00
POST to an external service on new sessions:
```yaml
# ~/.hermes/hooks/session-webhook/HOOK.yaml
name: session-webhook
description: Notify external service on new sessions
events:
- session:start
- session:reset
```
```python
# ~/.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)
```
2026-03-24 18:34:14 -07:00
### How It Works
2026-03-05 05:24:55 -08:00
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
2026-03-24 18:34:14 -07:00
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 ).
2026-03-05 05:24:55 -08:00
:::
2026-03-24 18:34:14 -07:00
## Plugin Hooks
[Plugins ](/docs/user-guide/features/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.
```python
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` |
2026-03-28 15:26:35 -07:00
| `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` |
2026-03-24 18:34:14 -07:00
Callbacks receive keyword arguments matching the columns above:
```python
def my_callback(**kwargs):
tool = kwargs["tool_name"]
args = kwargs["args"]
# ...
```
### Example: Block Dangerous Tools
```python
# ~/.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 ](/docs/user-guide/features/plugins )** for full details on creating plugins.