P30 30_进化篇_钩子函数
P30 · 30_进化篇_钩子函数(6:59)
课时概要
进化篇 · 钩子函数。钩子是 Hermes 在关键节点插入自定义逻辑的机制,官方文档有专门页面;仓库里按用途分成几类钩子脚本。
本分P 没有逐P 的官方简介(视频简介只有资源链接)。以下「本节要点 / 关键概念 / 代码实操」均改写自 Hermes 官方文档 Hooks 页(含 Gateway Event Hooks / Plugin Hooks / Shell Hooks / Outbound Webhooks 四节)与官方仓库原文并标注来源;「我的收获」里标 本机实测 的是在本机实跑得到的真实状态。
视频:30_进化篇_钩子函数 | 时长 6:59 | P30
本节要点
- 钩子一共四件套,官方给了对照表:Shell hooks | Plugin hooks | Gateway hooks | Outbound webhooks。前三者是「被事件触发」,第四个是「把事件推出去」。
- 四者对照:
· Shell hooks —— 声明在
config.yaml的hooks:块 | 脚本按约定放~/.hermes/agent-hooks/| 任意语言(Bash / Python / Go 二进制…)| 跑在 CLI + Gateway | 事件 =VALID_HOOKS(含subagent_stop)| 能拦工具调用(pre_tool_call)| 能注入 LLM 上下文(pre_llm_call)| 同意 = 每个(event, command)首次提示 | 有进程隔离(子进程) · Plugin hooks —— 在插件的register()里ctx.register_hook(...)| 仅 Python | CLI + Gateway | 同样能拦能注入 | 同意 = 显式plugins.enabled| 无进程隔离(进程内) · Gateway hooks ——~/.hermes/hooks/<名字>/目录 +HOOK.yaml+handler.py| 仅 Python | 只在 Gateway | 事件 = gateway 生命周期(gateway:startup、agent:*、command:*)| 不能拦、不能注入 | 同意 = 隐式(目录信任) · Outbound webhooks ——hooks.outbound:列表,事件发生时向你的 HTTP 端点 POST 已签名的 JSON - Shell hooks 用来做什么(官方原文四类):① 拦截或改写工具调用 —— 拒掉危险的
terminal命令、按目录施加策略、对破坏性write_file/patch要求审批、在工具执行前重写参数(清洗路径、注入默认值)② 工具调用后动作 —— 自动格式化 agent 刚写的文件、记 API 调用日志、触发 CI ③ 给下一轮 LLM 注入上下文 —— 把git status、今天是星期几、检索到的文档 prepend 到用户消息 ④ 观察生命周期 —— 子代理完成、会话开始时写日志。不需要写 Python 插件,一个单文件脚本就够。 - Gateway 事件钩子怎么建:
~/.hermes/hooks/my-hook/下两个文件 ——HOOK.yaml(name/description/events列表,支持command:*通配)+handler.py。handler 规则:函数必须叫handle、收event_type(字符串)与context(dict)、async def或普通def都行、出错只记日志,永不崩掉 agent。 - 可订阅的 gateway 事件(带 context keys):
gateway:startup(platforms)|session:start/session:end/session:reset|session:compress(带in_place布尔与compression_count)|agent:start(含message,截断 500 字符)/agent:step(含iteration、tool_names)/agent:end(多一个response)|reaction:added/reaction:removed(目前 Slack,需reactions:read范围 +reaction_added订阅)|command:*(任何斜杠命令,一个订阅监控所有命令)。 - 加载时机很关键:gateway 启动时加载一次(
HookRegistry.discover_and_load(),由gateway/run_startup.py调);多 profile 网关下,每个被服务 profile 自己的hooks/在该 profile 内首次触发事件时加载;⚠️ CLI / TUI / Desktop / cron 永不加载 gateway hooks。 - Shell hook 配置 schema:
hooks: <事件名>: - matcher: "<正则>"(可选,仅 pre/post_tool_call 用)/command: "<shell 命令>"(必填,经shlex.split执行、shell=False)/timeout(默认 60,上限 300)/fail_closed(默认 false,仅pre_tool_call);顶层还有hooks_auto_accept。容错行为:事件名拼错 → 出Did you mean X?警告并跳过;缺command→ 跳过并警告;timeout > 300会被夹到 300 并警告;fail_closed用在非pre_tool_call事件上 → 警告并忽略。 - JSON 线协议:每次事件触发,Hermes 为每个匹配的钩子起一个子进程,把 JSON 负载管道进 stdin,再从 stdout 读回 JSON。stdin 负载含
hook_event_name/tool_name/tool_input/session_id/cwd/profile(哪个 profile 触发的 —— 所以一个脚本可以服务多路复用网关下的所有 profile;子进程也带该 profile 的HERMES_HOME)/extra(事件专属 kwargs,如user_message、conversation_history、child_role、duration_ms)。非工具事件(pre_llm_call、subagent_stop、会话生命周期)的tool_name/tool_input为null。 - stdout 能返回的指令:block(Claude Code 风格
{"decision":"block","reason":…}或 Hermes 规范{"action":"block","message":…},两种都收)| modify(在执行前改写工具参数)| approve(把这次调用升级到人工审批闸门)| 注入上下文({"context": "..."})| continue(verify 门)| 空输出 = 静默 no-op。⚠️ 官方特别提醒:Claude Code 的{"decision":"approve"}意思是「自动放行」,与这里的 approve(升级到人工审批)不等价,不做映射。 - 容错:JSON 格式错误、非零退出码、超时都只记警告,永不中断 agent 循环。
exit code 2 = block(Claude Code / Cursor 兼容):pre_tool_call钩子即使 stdout 不带 block JSON,只要退出码是 2 就阻断。拦截消息按优先级解析:① stdout 的 block JSON(reason/message)② stderr 的前 400 字符 ③ 通用默认"Blocked by shell hook."。所以最简单的阻断钩子就是三行 bash。对不能阻断的事件(除pre_tool_call之外的全部),退出码 2 只当普通非零退出处理(记警告,仍解析 stdout)。- 官方四个实战范例:① 每次写文件后自动格式化 Python(
post_tool_call+ matcherwrite_file|patch)② 阻断破坏性的terminal命令(pre_tool_call+ 正则匹配rm -rf /后回 block JSON)③ 每轮注入git status(对应用户提示词提交钩子那一类需求)④ 记录每个子代理完成(subagent_stop)。 - ⚠️ 一个容易误会的细节(官方特别标注):
post_tool_call里自动格式化只改磁盘上的文件 —— agent 上下文里对那个文件的视图不会自动重读,要后续read_file才会拿到格式化后的版本。 - Outbound webhooks(出站):入站 webhook 是「世界变了叫醒 Hermes」,出站 webhook 是「Hermes 做了事告诉世界」。配
hooks.outbound:列表(name标签 /url/events列表 /secret_env(放 HMAC 密钥的环境变量名)/timeout每尝试秒数 1–60),匹配事件发生时 POST 已签名的 JSON,接收端不需要轮询。典型用法:回合结束时通知 CI 或看板(on_session_end)|跨舰队追踪子代理完成(subagent_stop)|把工具活动喂给外部监控(post_tool_call+ matcher)|叫醒另一个 Hermes 实例(URL 指向那个实例的入站 webhook)。 hermes hooksCLI:list(列出已配置钩子,含 matcher / timeout / 同意状态)|test <event> [--for-tool X] [--payload-file F](用合成负载打所有匹配钩子并打印解析后的响应)|revoke <command>(移除匹配该命令的所有 allowlist 条目,下次重启生效)|doctor(对每个已配置钩子检查:可执行位、allowlist 状态、mtime 漂移、JSON 输出合法性、粗测执行时间)。
关键概念
- Shell hook:
config.yaml里声明的外部脚本,事件触发时作为子进程运行,走 JSON 线协议 —— 唯一有进程隔离的一类。 - Plugin hook:插件在
register()里注册的 Python 回调,进程内运行(无隔离)。 - Gateway hook:
~/.hermes/hooks/<name>/下的HOOK.yaml+handler.py,只在 gateway 里跑、只能观察不能干预。 - Outbound webhook:把生命周期事件以 HMAC 签名的 JSON 推给外部端点。
- JSON 线协议:stdin 收负载、stdout 回指令(block / modify / approve / context / continue / 空)。
- matcher:正则,只在
pre_tool_call/post_tool_call上生效,用来筛工具名。 handle:handler.py里必须存在的入口函数名。extra:各事件专属的 kwargs 载体(user_message、duration_ms…)。
代码 / 实操
Gateway 事件钩子(~/.hermes/hooks/my-hook/):
yaml
# HOOK.yaml
name: my-hook
description: Log all agent activity to a file
events:
- agent:start
- agent:end
- agent:steppython
# handler.py —— 函数必须叫 handle
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):
entry = {"timestamp": datetime.now().isoformat(), "event": event_type, **context}
with open(LOG_FILE, "a") as f:
f.write(json.dumps(entry) + "\n")Shell hook(config.yaml):
yaml
hooks:
pre_tool_call:
- matcher: "terminal"
command: "~/.hermes/agent-hooks/block-rm-rf.sh"
timeout: 5
post_tool_call:
- matcher: "write_file|patch"
command: "~/.hermes/agent-hooks/auto-format.sh"
hooks_auto_accept: falsebash
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/block-rm-rf.sh —— 从 stdin 读 JSON、向 stdout 回 JSON
payload="$(cat -)"
cmd=$(echo "$payload" | jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -qE 'rm[[:space:]]+-rf?[[:space:]]+/'; then
printf '{"decision": "block", "reason": "blocked: rm -rf / is not permitted"}\n'
else
printf '{}\n'
fibash
hermes hooks list # 已配置的钩子 + 同意状态
hermes hooks test pre_tool_call --for-tool terminal
hermes hooks doctor # 可执行位 / allowlist / mtime 漂移 / JSON 合法性本机实测(2026-09-25):
$ hermes hooks list
No shell hooks or outbound webhooks configured in ~/.hermes/config.yaml.
$ hermes hooks doctor
No shell hooks configured — nothing to check.
$ ls -la ~/.hermes/hooks/ → 空目录(0 文件,创建于 4/17 01:30)
$ ls ~/.hermes/agent-hooks/ → 不存在
$ ~/.hermes/config.yaml → 没有 hooks: 段
仓库实现文件的真实体积:agent/shell_hooks.py 31,551 B(最大)| agent/outbound_webhooks.py 15,156 B | agent/auxiliary_hooks.py 10,531 B | agent/api_request_hooks.py 8,208 B | gateway/hooks.py 8,184 B | agent/plugin_stream_hooks.py 6,748 B | agent/verify_hooks.py 2,298 B。
我的收获
用户此前已学过本模块;以下是本机实测状态(2026-09-25):
- 本机一个钩子都没配(三重实证):
hermes hooks list明确回「No shell hooks or outbound webhooks configured」;hooks doctor回「nothing to check」;~/.hermes/agent-hooks/目录根本不存在、config.yaml 里也没有hooks:段。 ~/.hermes/hooks/(gateway 事件钩子的位置)存在但是空的 —— 这正好印证官方那句「放文件即 opt-in」:目录在但没内容 = 没有任何 gateway 钩子在跑。- 仓库里七类钩子实现文件的体积分布很说明问题:
shell_hooks.py以 31.5 KB 远超其余(第二名outbound_webhooks.py15 KB)—— 与官方对它的定位一致(唯一能拦截、唯一有进程隔离、唯一支持任意语言,所以实现最重)。 - 💡 对这台机器最实用的判断:hooks 是零依赖就能加的护栏层 —— 我平时最缺的两类自动化(「写后自动格式化」「阻断危险命令」)官方都给了现成范例,而且不需要写 Python 插件,一个 bash 脚本 + config 三行就够。
- ⚠️ 一个必须记住的陷阱:本机
hooks_auto_accept未设(默认 false),而官方明确说非 TTY 运行(gateway / cron / CI)需要三个逃生舱之一,否则新加的钩子会静默保持未注册、只留一条警告 —— 也就是说「我配了钩子但它在 gateway 里没生效」很可能是这个原因,不是钩子写错了。
(个人感想部分待补写。)
待深入
(待填:没听懂、想回头查的。)