跳转到内容

事件钩子(Event Hooks)

「想在 Telegram 上每次长任务超过 10 步就收到提醒,却改动了核心仓库——其实只需在 ~/.hermes/hooks/ 放一个 Gateway 钩子。」

技能系统实战 之后,你可能需要在 Agent 生命周期里插入自己的逻辑:记录 Slash 使用、阻断危险工具、或在每轮对话前注入 git status。Hermes 提供三套互不替代的钩子体系。依据官方 Hooks

前置:已能跑通 CLI 或 Gateway;若用 Plugin 钩子,需理解 工具系统 中的审批与 pre_tool_call 概念。

体系注册方式运行范围典型用途能否阻断工具
Gateway hooks~/.hermes/hooks/<name>/HOOK.yaml + handler.py仅 Gateway(Telegram、Discord 等)启动清单、Webhook、步数告警
Plugin hooks插件 register()ctx.register_hook()CLI + Gateway审计、策略、pre_llm_call 注入是(pre_tool_call
Shell hooksconfig.yamlhooks: 块指向脚本CLI + GatewayBash 一键格式化、阻断 rm是(pre_tool_call

三套钩子错误均被捕获并记录,不会拖垮 Agent 主循环。这与审批、沙箱的关系是:钩子可提前否决或改写行为;审批与 hardline 仍是内置地板

用户消息 → Gateway/CLI
→ [pre_gateway_dispatch](仅 Gateway,Plugin)
→ Agent Loop
→ [pre_llm_call] 可注入上下文
→ 工具循环
→ [pre_tool_call] 可 block
→ 执行工具(含审批)
→ [post_tool_call] / [transform_tool_result]
→ [transform_llm_output]
→ [agent:* Gateway hooks] 观测用

Gateway 钩子在 gateway:startupagent:startagent:stepcommand:* 等时刻触发,不阻塞主 Agent 管线,适合日志与外部通知。

~/.hermes/hooks/my-hook/
├── HOOK.yaml
└── handler.py

HOOK.yaml

name: my-hook
description: 记录 agent 活动
events:
- agent:start
- agent:end
- agent:step

handler.py(函数名必须为 handle,支持 async def):

import json
from datetime import datetime
from pathlib import Path
LOG = Path.home() / ".hermes" / "hooks" / "my-hook" / "activity.log"
async def handle(event_type: str, context: dict):
entry = {"ts": datetime.now().isoformat(), "event": event_type, **context}
LOG.parent.mkdir(parents=True, exist_ok=True)
with open(LOG, "a") as f:
f.write(json.dumps(entry) + "\n")

重启 Gateway 后,在 Telegram 发一条消息,检查日志是否出现 agent:start / agent:end

事件时机典型 context
gateway:startupGateway 进程启动platforms
session:start / session:end / session:reset会话生命周期platform, user_id, session_key
agent:start / agent:end处理一条用户消息message, response
agent:step工具循环每一步iteration, tool_names
command:*任意 Slashcommand, args

订阅 command:* 可一次监听所有 Slash,无需逐个列举。

官方教程演示:在 ~/.hermes/BOOT.md 写自然语言清单,用 gateway:startup 钩子 spawn 一次性 Agent 执行。要点:

  • _resolve_gateway_model()_resolve_runtime_agent_kwargs(),避免裸 AIAgent() 导致自定义端点 401。
  • 后台线程执行,不阻塞 Gateway 启动。
  • Agent 无事项可报时回复 [SILENT],钩子不对外发消息。

误用边界:Gateway 钩子不会在纯 CLI 会话触发;要全环境生效请用 Plugin 或 Shell 钩子。

已启用插件在 register(ctx) 中注册,CLI 与 Gateway 共用同一分发器。

def register(ctx):
ctx.register_hook("pre_tool_call", audit_tool)
ctx.register_hook("pre_llm_call", inject_context)
ctx.register_hook("transform_tool_result", redact_secrets)

回调应接受 **kwargs,便于未来扩展参数。

钩子返回值作用
pre_tool_call{"action": "block", "message": "..."} 否决工具,模型看到错误信息
pre_llm_call{"context": "..."} 或纯字符串,追加到当前轮用户消息(不改 system,利于 Prompt Cache)
transform_tool_result非空 str 替换工具结果
transform_terminal_output在 terminal 截断/脱敏前改写原始输出
transform_llm_output改写最终助手文本

其余钩子(post_tool_callpost_llm_callon_session_* 等)为观察型,返回值被忽略。

场景推荐机制说明
通用危险命令(rm -rf /hardline + approvals内置地板,钩子不必重复
组织策略(禁止某目录 write_filepre_tool_call 插件/Shell可带业务规则
每轮注入 RAG / 记忆pre_llm_call不污染 system 前缀
Gateway 步数告警Gateway agent:step不阻塞,只通知
对外 WebhookGateway session:start与工具无关

注册顺序pre_tool_call 的 block 以先注册者优先(Python 插件先于 Shell 钩子)。多个插件都返回 block 时,第一个生效。

def inject_git_status(session_id, user_message, is_first_turn, **kwargs):
import subprocess
try:
out = subprocess.check_output(
["git", "status", "--short"], text=True, timeout=3
)
if out.strip():
return {"context": f"Git status:\n{out}"}
except Exception:
pass
return None

注入内容仅本轮 API 调用可见,不写入 SessionDB 中的原始用户消息。

~/.hermes/config.yaml 声明,脚本放在 ~/.hermes/agent-hooks/ 等路径,无需写 Python 插件

hooks:
pre_tool_call:
- matcher: "terminal"
command: "~/.hermes/agent-hooks/block-dangerous.sh"
timeout: 10
post_tool_call:
- matcher: "write_file|patch"
command: "~/.hermes/agent-hooks/auto-format.sh"
hooks_auto_accept: false

Hermes 向 stdin 写入 JSON,从 stdout 读取 JSON 响应。pre_tool_call 可返回:

{"action": "block", "message": "禁止删除系统路径"}

首次运行某 (event, command) 对会提示确认(除非 hooks_auto_accept: true),避免静默执行未知脚本。

维度ShellPluginGateway
语言任意可执行脚本PythonPython
进程隔离子进程进程内进程内
plugins.enabled是(通用插件)否(目录即加载)
适合团队运维脚本可分发 pip 包仅消息平台侧效应
能力钩子SkillMCP
教 Agent 怎么做提供新工具
强制策略否(建议性)
跨会话程序性知识
观测 Gateway 步数Gateway 钩子

不要把应用层业务规则写进 SOUL.md 指望模型自觉遵守;应使用 pre_tool_call 或审批。

现象可能原因处理
CLI 里钩子不触发用了 Gateway 专用目录改 Plugin/Shell
pre_tool_call 无效插件未 plugins.enabledhermes plugins enable <name>
Shell 钩子静默失败JSON 格式错、超时~/.hermes/logs/;降 matcher 范围
BOOT 清单 401未用 gateway 凭据解析对照官方 BOOT 教程改 handler
注入无效返回了空 context确认 pre_llm_call 返回值非空
  • 不要用 Gateway 钩子做工具阻断:它没有 pre_tool_call 语义。
  • 不要在 pre_llm_call 塞入整份仓库:用 @filesession_search;见 第一次对话
  • 不要默认 hooks_auto_accept: true 于不可信机器:等同自动执行任意脚本。
  • 不要与 hardline 重复造轮子rm -rf / 已被内置拒绝,钩子应补组织特有规则。
  1. 创建 command-logger Gateway 钩子,订阅 command:*,记录 3 次 Slash 使用。
  2. 写一个 Shell post_tool_call 钩子,在 write_file 修改 .py 后尝试调用 black(若已安装)。
  3. 在测试插件里注册 pre_tool_call,对 tool_name == "terminal" 且命令含 /etc 时 block,验证模型收到 block 消息。
  4. 对比同一会话在 CLI 与 Gateway 下 Gateway 钩子是否仅后者触发。
  • 说出三套钩子的运行范围差异。
  • 解释 pre_llm_call 为何注入用户消息而非 system。
  • 写出 pre_tool_call block 的 JSON 形状。
  • 说明 Gateway 钩子与审批的分工。

下一章:Kanban 多 Agent 看板,对比 delegate_task 与持久任务队列的取舍。