| --- |
| sidebar_position: 6 |
| title: "Event Hooks" |
| description: "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](#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 |
|
|
| Each hook is a directory under `~/.hermes/hooks/` containing two files: |
|
|
| ```text |
| ~/.hermes/hooks/ |
| βββ my-hook/ |
| βββ HOOK.yaml # Declares which events to listen for |
| βββ handler.py # Python handler function |
| ``` |
|
|
| #### HOOK.yaml |
|
|
| ```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 |
|
|
| ```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 |
|
|
| ### 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: |
|
|
| ```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}, |
| ) |
| ``` |
|
|
| #### Command Usage Logger |
|
|
| 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") |
| ``` |
|
|
| #### Session Start Webhook |
|
|
| 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) |
| ``` |
|
|
| ### 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 |
| 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). |
| ::: |
|
|
| ## 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` | |
| | `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: |
|
|
| ```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. |
|
|