检查点、回滚与 Hooks 钩子
本教程共 25 篇 · 第 20 篇 · 更新于 2026-07-26 · 约 16 分钟阅读
20. 检查点、回滚与 Hooks 钩子
本节目标:搞懂 Hermes Agent 给破坏性操作准备的两道安全网。第一道是检查点(Checkpoint)+
/rollback,在 Agent 改文件前自动快照、出事一键回滚。第二道是三套 Hooks 系统,让你在生命周期关键时刻插自定义逻辑—审计、拦截、注入上下文、自动格式化。学完你能开启检查点保护项目、写出自己的 hook。
检查点:Agent 改文件前的安全网
先打个比方。你让一个实习生改一份重要文档,聪明做法是改之前先复制一份「改前版本」放旁边。要是他改崩了,你拿备份一替就回去了。
Hermes Agent 的检查点(Checkpoint)就是这个机制。Agent 在执行破坏性操作之前,会自动给项目拍个快照存到一边。出事了,一条 /rollback 命令就能把项目恢复到任意一个快照。
这套机制由内部的 Checkpoint Manager 驱动,底层用一个共享的 shadow git 仓库(藏在 ~/.hermes/checkpoints/store/),绝不碰你项目里真正的 .git。所有 Agent 工作过的项目共用同一个 store,所以 git 的内容寻址对象库会在项目之间、轮次之间自动去重。
Note检查点是 opt-in 的,默认关闭。理由是大多数用户根本不用
/rollback,而 shadow store 的存储开销长期看不小。要用了再开。
怎么开
按会话开:
hermes chat --checkpoints
全局开,在 ~/.hermes/config.yaml:
checkpoints:
enabled: true
什么时候会触发快照
Agent 在这些操作之前自动拍快照:
- 文件工具:
write_file和patch - 破坏性终端命令:
rm、rmdir、cp、install、mv、sed -i、truncate、dd、shred、输出重定向(>)、git reset/clean/checkout
每个目录每轮对话最多拍一个快照,长会话不会刷屏。
/rollback 怎么用
会话里的斜杠命令:
| 命令 | 作用 |
|---|---|
/rollback | 列出所有检查点,带变更统计 |
/rollback <N> | 恢复到第 N 号检查点(同时撤销最近一轮对话) |
/rollback diff <N> | 预览第 N 号检查点和当前状态的 diff |
/rollback <N> <file> | 从第 N 号检查点恢复单个文件 |
/rollback 列出来长这样:
📸 Checkpoints for /path/to/project:
1. 4270a8c 2026-03-16 04:36 before patch (1 file, +1/-0)
2. eaf4c1f 2026-03-16 04:35 before write_file
3. b3f9d2e 2026-03-16 04:34 before terminal: sed -i s/old/new/ config.py (1 file, +1/-1)
/rollback <N> restore to checkpoint N
/rollback diff <N> preview changes since checkpoint N
/rollback <N> <file> restore a single file from checkpoint N
恢复时 Hermes 在背后做四件事:
- 验证目标 commit 在 shadow store 里存在
- 给当前状态拍一个回滚前快照,让你之后能「撤销这次撤销」
- 把工作目录里被追踪的文件恢复回去
- 撤销最近一轮对话,让 Agent 的上下文和恢复后的文件系统状态对齐
只想恢复一个文件、不动其他部分,用单文件恢复:
/rollback 1 src/broken_file.py
在 shell 里看 store
hermes checkpoints
输出大概长这样:
Checkpoint base: /home/you/.hermes/checkpoints
Total size: 142.3 MB
store/ 138.1 MB
legacy-* 4.2 MB
Projects: 12
WORKDIR COMMITS LAST TOUCH STATE
/home/you/code/hermes-agent 20 2h ago live
/home/you/code/experiments/rl-runner 8 1d ago live
/home/you/code/old-prototype 3 9d ago orphan
...
Legacy archives (1):
legacy-20260506-050616 4.2 MB
Clear with: hermes checkpoints clear-legacy
CLI 命令一览:
| 命令 | 作用 |
|---|---|
hermes checkpoints | 显示总大小、项目数、每项目明细 |
hermes checkpoints status | 同上 |
hermes checkpoints list | status 别名 |
hermes checkpoints prune | 强制清扫:删孤儿/过期、GC、按上限裁 |
hermes checkpoints clear | 清空整个检查点库(会先问) |
hermes checkpoints clear-legacy | 只删旧版迁移留下的 legacy-* 归档 |
强制完整清扫(忽略 24 小时幂等标记):
hermes checkpoints prune --retention-days 3 --max-size-mb 200
配置
checkpoints:
enabled: false # 主开关(默认 false,opt-in)
max_snapshots: 20 # 每项目最大检查点数(靠 ref 重写 + gc 执行)
max_total_size_mb: 500 # store 总大小硬上限;超了丢最老 commit
max_file_size_mb: 10 # 单文件超过这个大小就跳过
# 自动维护(默认开):启动时扫 ~/.hermes/checkpoints/,
# 删 last_touch 早于 retention_days 的项目条目。
# 至多每 min_interval_hours 跑一次,靠 .last_prune 标记追踪。
# 这个清扫绝不删「孤儿」条目(工作目录找不到)--启动时工作目录缺失
# 是有歧义的(项目被删 vs 外接卷/网络共享/VPN 还没起来),
# 所以孤儿清理只能靠显式 `hermes checkpoints prune` 命令,带确认提示。
auto_prune: true
retention_days: 7
min_interval_hours: 24
全关:
checkpoints:
enabled: false
auto_prune: false
enabled: false 时 Checkpoint Manager 是 no-op,从不尝试 git 操作。auto_prune: false 时 store 一直长,直到你手动 hermes checkpoints prune。
安全与性能保护
- git 不可用:PATH 里找不到
git时,检查点透明关闭 - 目录范围:Hermes 跳过过宽的目录(根
/、家目录$HOME) - 仓库大小:超过 5 万个文件的目录跳过
- 单文件大小上限:超过
max_file_size_mb(默认 10 MB)的文件排除在快照外,防止误吞数据集、模型权重、生成的媒体 - 总 store 大小上限:超过
max_total_size_mb(默认 500 MB)时,按项目轮询丢最老 commit 直到回到上限下 - 真正修剪:
max_snapshots靠重写每项目 ref + 之后跑git gc --prune=now执行,松散对象不会堆积 - 无变更快照:自上次快照没变化就跳过
- 非致命错误:Checkpoint Manager 内所有错误按 debug 级别记日志;你的工具继续跑
检查点放哪
~/.hermes/checkpoints/
├── store/ # 单个共享 bare git 仓库
│ ├── HEAD, objects/ # git 内部(跨项目共享)
│ ├── refs/hermes/<hash> # 每项目分支 tip
│ ├── indexes/<hash> # 每项目 git index
│ ├── projects/<hash>.json # workdir + created_at + last_touch
│ └── info/exclude
├── .last_prune # 自动修剪幂等标记
└── legacy-<ts>/ # 归档的旧版每项目 shadow 仓库
每个 <hash> 由工作目录的绝对路径派生。你一般不用手动碰这些—用 hermes checkpoints status / prune / clear 就行。
最佳实践
- 需要时再开:
hermes chat --checkpoints或按 profileenabled: true - 恢复前先 diff:用
/rollback diff预览要改什么,挑对检查点 - 想只撤销 Agent 改的、不动你自己提交的:用
/rollback而不是git reset - 常用就定期查
hermes checkpoints status:看哪些项目活跃、store 花了你多少 - 配 Git worktree 最安全:每个 Hermes 会话放自己的 worktree/branch,检查点再加一层
Hooks:在生命周期关键时刻插自定义逻辑
检查点是 Hermes 内置的安全网。Hooks 是你给 Hermes 加的自定义钩子—在生命周期的关键时刻(启动、会话开始、工具调用前后、LLM 调用前后、会话结束)跑你自己的一段代码。
打个比方,Hooks 就像装修时给各种电器装的智能开关:热水器启动前先放水排冷空气、空调关后延时关窗、冰箱开门超时报警。电器本体(核心循环)不用改,你只是在它的生命周期节点上挂了点额外动作。
Hermes 有三套 Hooks 系统,面向不同场景:
| 系统 | 注册方式 | 跑在哪 | 用途 |
|---|---|---|---|
| Gateway hooks | ~/.hermes/hooks/ 下的 HOOK.yaml + handler.py | 仅 Gateway | 日志、告警、webhook |
| Plugin hooks | 插件里的 ctx.register_hook() | CLI + Gateway | 工具拦截、指标、护栏 |
| Shell hooks | ~/.hermes/config.yaml 的 hooks: 块指向 shell 脚本 | CLI + Gateway | 拖入脚本做拦截、自动格式化、上下文注入 |
三套都非阻塞—任何 hook 里的错误都被捕获并记日志,绝不崩 Agent。
Gateway 事件 hooks
Gateway hooks 在 Gateway 运行期间(Telegram、Discord、Slack、WhatsApp、Teams)自动触发,不阻塞主 Agent 管道。
创建一个 hook
每个 hook 是 ~/.hermes/hooks/ 下的一个目录,含两个文件:
~/.hermes/hooks/
└── my-hook/
├── HOOK.yaml # 声明监听哪些事件
└── handler.py # Python 处理函数
HOOK.yaml:
name: my-hook
description: Log all agent activity to a file
events:
- agent:start
- agent:end
- agent:step
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):
"""每个订阅事件都会调。必须叫 'handle'。"""
entry = {
"timestamp": datetime.now().isoformat(),
"event": event_type,
**context,
}
with open(LOG_FILE, "a") as f:
f.write(json.dumps(entry) + "\n")
handler 规则:
- 必须叫
handle - 收
event_type(字符串)和context(dict) async def或普通def都行- 错误被捕获并记日志,绝不崩 Agent
可用事件
| 事件 | 触发时机 | context 键 |
|---|---|---|
gateway:startup | Gateway 进程启动 | platforms(活跃平台名列表) |
session:start | 新消息会话创建 | platform, user_id, session_id, session_key |
session:end | 会话结束(重置前) | platform, user_id, session_key |
session:reset | 用户跑了 /new 或 /reset | platform, user_id, session_key |
agent:start | Agent 开始处理消息 | platform, user_id, session_id, message |
agent:step | 工具调用循环每轮迭代 | platform, user_id, session_id, iteration, tool_names |
agent:end | Agent 处理完成 | platform, user_id, session_id, message, response |
reaction:added | 给 bot 能看到的某条消息加了表情(Slack 适配器目前)。需要 reactions:read scope + reaction_added bot 事件订阅;bot 必须是频道成员 | platform, reaction, user_id, item_user_id, item_type, channel_id, message_ts, team_id, event_ts, raw_event |
reaction:removed | 表情被移除。需要 reaction_removed bot 事件订阅 | 同 reaction:added |
command:* | 任何斜杠命令执行 | platform, user_id, command, args |
通配符匹配
注册 command:* 的 handler 会对任何 command: 事件触发(command:model、command:reset 等)。一次订阅监控所有斜杠命令。
经典模式:BOOT.md 启动清单
社区流行一个模式:在 ~/.hermes/BOOT.md 放一份 Markdown 清单,Gateway 每次启动时让 Agent 跑一遍。适合「每次启动检查昨晚 cron 失败没,有事就在 Discord 喊我」或「总结最近 24 小时 deploy.log 发到 Slack #ops」。
Hermes 不内置这个 hook,你自己照下面搭:
- 写
~/.hermes/BOOT.md:
# Startup Checklist
1. Run `hermes cron list` and check if any scheduled jobs failed overnight.
2. If any failed, summarize them for Discord #ops (the hook delivers your final response to its configured target).
3. Check if `/opt/app/deploy.log` has any ERROR lines from the last 24 hours. If yes, summarize them and include in the same report.
4. If nothing went wrong, reply with only `[SILENT]` so no message is sent.
-
建 hook 目录
~/.hermes/hooks/boot-md/,HOOK.yaml订阅gateway:startup,handler.py在后台线程起一个一次性 Agent,用 Gateway 解析出来的 model 和凭据跑BOOT.md内容。Agent 看到这是它的 prompt,所以你平铺直叙写就行—工具调用、shell 命令、发消息、总结文件都行。 -
重启 Gateway 测试:
hermes gateway restart
hermes logs --follow --level INFO | grep boot-md
应该看到 Running BOOT.md (N chars),之后是 boot-md completed: ...(Agent 做了啥的摘要)或 boot-md completed (nothing to report)(Agent 回了精确的静默 token 如 [SILENT])。
Tip为什么不内置:早期版本把这个当内置 hook,每次 Gateway 启动就用裸默认值静默起一个 Agent。这惊到了用自定义端点的用户,也让不知道它在跑的用户看不见这功能。现在留作文档模式—你自己在 hooks 目录里搭,看到它具体干啥,写文件才算启用。
工作原理
- Gateway 启动时,
HookRegistry.discover_and_load()扫~/.hermes/hooks/ - 每个含
HOOK.yaml+handler.py的子目录被动态加载 - handler 按声明的事件注册
- 每个生命周期点,
hooks.emit()触发所有匹配 handler - 任何 handler 的错误被捕获并记日志—坏 hook 永不崩 Agent
NoteGateway hooks 只在 Gateway(Telegram、Discord、Slack、WhatsApp、Teams)里触发。CLI 不加载 Gateway hooks。要到处都跑的 hook,用 Plugin hooks 或 Shell hooks。
Plugin hooks
插件 能注册在 CLI 和 Gateway 都触发的 hook,通过插件的 register() 函数里 ctx.register_hook() 编程注册。
def register(ctx):
ctx.register_hook("pre_tool_call", my_tool_observer)
ctx.register_hook("post_tool_call", my_tool_logger)
ctx.register_hook("pre_llm_call", my_memory_callback)
ctx.register_hook("post_llm_call", my_sync_callback)
ctx.register_hook("on_session_start", my_init_callback)
ctx.register_hook("on_session_end", my_cleanup_callback)
通用规则
- 回调收关键字参数。永远接
**kwargs为向前兼容—未来版本可能加新参数不破坏你的插件 - 回调崩溃了就记日志跳过。其他 hook 和 Agent 照常跑。坏插件永不崩 Agent
- 两个 hook 的返回值会影响行为:
pre_tool_call能拦截工具,pre_llm_call能向 LLM 调用注入上下文。其他都是 fire-and-forget 观察者 - 观察者回调自动收
telemetry_schema_version。出现时,turn_id、api_request_id、task_id、session_id、api_call_count是独立的关联字段。把api_request_id当不透明标识符,别解析它的字符串格式
快速参考
| Hook | 触发时机 | 返回值 |
|---|---|---|
pre_tool_call | 任何工具执行前 | {"action": "block", "message": str} 否决调用 |
post_tool_call | 任何工具返回后 | 忽略 |
pre_llm_call | 每轮一次,工具调用循环开始前 | {"context": str} 在用户消息前注入上下文 |
post_llm_call | 每轮一次,工具调用循环完成后 | 忽略 |
pre_verify | 每轮一次,Agent 改了代码、即将验证/完成时 | {"action": "continue", "message": str} 让它继续 |
on_session_start | 新会话创建(仅首轮) | 忽略 |
on_session_end | 会话结束 | 忽略 |
on_session_finalize | CLI/Gateway 拆掉活跃会话(flush、保存、统计) | 忽略 |
on_session_reset | Gateway 换新会话 key(如 /new、/reset) | 忽略 |
subagent_start | delegate_task 子 Agent 构造完、即将运行 | 忽略 |
subagent_stop | delegate_task 子 Agent 退出 | 忽略 |
pre_gateway_dispatch | Gateway 收到用户消息、auth + 派发前 | `{“action”: “skip" |
pre_approval_request | 请求批准决策(含 smart 模式自动决策) | 忽略 |
post_approval_response | 批准决策做出(或提示超时) | 忽略 |
transform_tool_result | 任何工具返回后、结果交给模型前 | str 替换结果,None 不变 |
transform_terminal_output | terminal 工具内、截断/ANSI 剥离/脱敏前 | str 替换原始输出,None 不变 |
transform_llm_output | 工具调用循环完成后、最终响应交给用户前 | str 替换响应文本,None/空不变 |
pre_tool_call:拦截危险工具
每个工具执行前立即触发,内置工具和插件工具都一样。
def my_callback(tool_name: str, args: dict, task_id: str, **kwargs):
返回值能否决调用:
return {"action": "block", "message": "Reason the tool call was blocked"}
Agent 用 message 作为错误返回给模型短路掉这个工具。第一个匹配的 block 指令赢(Python 插件先注册,然后是 shell hooks)。
适用:日志、审计、工具调用计数器、拦截危险操作、限速、按用户策略。
DANGEROUS = {"terminal", "write_file", "patch"}
def warn_dangerous(tool_name, **kwargs):
if tool_name in DANGEROUS:
print(f"⚠ Executing potentially dangerous tool: {tool_name}")
def register(ctx):
ctx.register_hook("pre_tool_call", warn_dangerous)
pre_llm_call:注入上下文
每轮一次,工具调用循环开始前。这是唯一返回值会被用的 hook—它能给当前轮的用户消息注入上下文。
def my_callback(session_id: str, user_message: str, conversation_history: list,
is_first_turn: bool, model: str, platform: str, **kwargs):
返回带 "context" 键的 dict 或非空字符串,文本会附加到当前轮用户消息。None 不注入。
# 注入上下文
return {"context": "Recalled memories:\n- User likes Python\n- Working on hermes-agent"}
# 不注入
return None
Note注入位置:永远在用户消息,不是系统 prompt。这保留 prompt 缓存—系统 prompt 跨轮保持字节稳定,缓存 token 复用。系统 prompt 是 Hermes 的地盘(模型引导、工具强制、人格、技能)。插件在用户输入旁边贡献上下文。所有注入的上下文都是临时的—只在 API 调用时加。对话历史里的原始用户消息永不改变,也不持久化到会话数据库。
pre_verify:验证门(verify-hooks)
每轮一次,Agent 改了代码、即将完成时触发(在内置的 verify-on-stop 守卫之后)。这是用户/插件策略门:回调能让 Agent 继续—跑个检查、推迟它、整理 diff—而不是让它停。
def my_callback(session_id: str, platform: str, model: str, coding: bool,
attempt: int, final_response: str, changed_paths: list, **kwargs):
返回值让它继续:
return {"action": "continue", "message": "Run the formatter on your changes, then finish."}
message 作为合成用户轮附加,循环再跑一遍。Claude-Code Stop 形态({"decision": "block", "reason": "..."},block 意味着继续)也接受。
有界:一轮里连续 continue 指令由 agent.max_verify_nudges(默认 3)封顶,所以总说 continue 的 hook 不会困住循环。被推开的尝试答案留在历史里,但 Agent 被推时不展示给用户。
做成幂等:hook 每次推开后再触发,所以靠 attempt 判断(if attempt: return None)—否则就一路推到上限。
适用:创作迭代时推迟测试/lint、要求某些路径有绿色检查、改 changelog 前不让说「完成」、跑项目特定验证清单。
UI = (".tsx", ".jsx", ".css", ".scss")
def defer_ui_checks(coding, attempt, changed_paths, **kwargs):
if attempt or not coding:
return None # 一次性,仅 coding
if not all(p.endswith(UI) for p in changed_paths):
return None # 仅纯 UI 编辑
return {
"action": "continue",
"message": "This is UI work - don't run tests/lints yet; ask the user to "
"eyeball it first, and clean the diff before any commit.",
}
def register(ctx):
ctx.register_hook("pre_verify", defer_ui_checks)
kanban-stop:看板工作者的终止守卫
Kanban 工作者(dispatcher 派生的 worker)必须以 kanban_complete 或 kanban_block 结束。但有些模型家族(GLM、Qwen 等)有时会口述下一步(「现在我来写报告」)然后以 finish_reason=stop 停下、不带工具调用。Hermes 把这当干净退出 -> rc=0 -> dispatcher 报 protocol_violation。
kanban-stop 是个策略模块:当 Kanban worker 试图不带终止板工具就完成时,返回一个有界的合成 nudge,让对话循环继续而不是退出。
启用条件:
- 环境变量
HERMES_KANBAN_TASK被设置(dispatcher 派生的 worker) - 除非
HERMES_KANBAN_STOP_NUDGE显式禁用(0/false/no/off)
默认最多尝试 2 次 nudge,超了就放行退出。这保证看板协作的协议契约,又不死锁。
subagent_start / subagent_stop:委托观察
subagent_start 在 delegate_task 构造完子 Agent、运行前触发,单任务或三任务批量都对每个子 Agent 触发一次。这是观察 hook,返回值不阻塞也不改子 Agent 运行。要拦委托,用 pre_tool_call 拦 delegate_task 工具调用。
subagent_stop 在子 Agent 完成后触发,串行化在父线程上,所以你不用担心并发回调。重委托(编排者 × 5 叶子 × 嵌套深度)时这 hook 一轮触发很多次,保持回调快,把贵活推到后台队列。
transform_*:改写结果与响应
三个 transform hook 让你用经典编程改写文本,不烧额外推理 token:
transform_tool_result:工具返回后、结果追加到对话前。能改任何工具的结果字符串transform_terminal_output:terminal工具内、50 KB 默认截断和 ANSI 剥离前。改 shell 命令的原始 stdout/stderrtransform_llm_output:每轮一次,工具循环完成后、最终响应交给用户前。改助手最终文本
适用:从 web_extract 输出脱敏组织特定 PII、给长 JSON 工具响应加摘要头、给 read_file 结果注入 RAG 提示、把 delegate_task 子 Agent 报告改写成项目特定 schema、给最终响应加人格变换(海盗腔、Spongebob)。
Shell hooks:拖入脚本不用写 Python
在 ~/.hermes/config.yaml 的 hooks: 块里声明 shell 脚本 hook,Hermes 会在对应插件 hook 事件触发时把它们当子进程跑—CLI 和 Gateway 都跑。不用写 Python 插件。
适用场景:
- 拦截工具调用:拒绝危险
terminal命令、按目录策略、要求write_file/patch批准 - 工具调用后跑:自动格式化 Agent 刚写的 Python 或 TypeScript 文件、记 API 调用、触发 CI
- 给下一轮 LLM 注入上下文:在用户消息前加
git status输出、当前星期几、检索到的文档(见pre_llm_call) - 观察生命周期事件:子 Agent 完成时(
subagent_stop)写日志、会话开始时(on_session_start)记一行
对比
| 维度 | Shell hooks | Plugin hooks | Gateway hooks |
|---|---|---|---|
| 声明在 | ~/.hermes/config.yaml 的 hooks: 块 | plugin.yaml 插件里的 register() | HOOK.yaml + handler.py 目录 |
| 住在 | ~/.hermes/agent-hooks/(约定) | ~/.hermes/plugins/<name>/ | ~/.hermes/hooks/<name>/ |
| 语言 | 任意(Bash、Python、Go 二进制…) | 仅 Python | 仅 Python |
| 跑在哪 | CLI + Gateway | CLI + Gateway | 仅 Gateway |
| 能拦工具调用 | 是(pre_tool_call) | 是(pre_tool_call) | 否 |
| 能注入 LLM 上下文 | 是(pre_llm_call) | 是(pre_llm_call) | 否 |
| 同意 | 每个 (event, command) 对首次使用提示 | 隐式(Python 插件信任) | 隐式(目录信任) |
| 进程隔离 | 是(子进程) | 否(进程内) | 否(进程内) |
配置 schema
hooks:
<event_name>: # 必须在 VALID_HOOKS 里
- matcher: "<regex>" # 可选;仅 pre/post_tool_call 用
command: "<shell command>" # 必填;用 shlex.split 跑,shell=False
timeout: <seconds> # 可选;默认 60,上限 300
hooks_auto_accept: false # 见下面「同意模型」
事件名必须是插件 hook 事件之一;拼错会给「Did you mean X?」警告并跳过。
JSON 线协议
每次事件触发,Hermes 为每个匹配 hook(matcher 允许的话)起一个子进程,把 JSON 通过 stdin 喂进去,从 stdout 读 JSON 回来。
stdin—脚本收到的 payload:
{
"hook_event_name": "pre_tool_call",
"tool_name": "terminal",
"tool_input": {"command": "rm -rf /"},
"session_id": "sess_abc123",
"cwd": "/home/user/project",
"extra": {"task_id": "...", "tool_call_id": "..."}
}
非工具事件(pre_llm_call、subagent_stop、会话生命周期)的 tool_name 和 tool_input 是 null。extra dict 装所有事件特定 kwargs(user_message、conversation_history、child_role、duration_ms…)。不能序列化的值会被字符串化而不是省略。
stdout—可选响应:
// 拦 pre_tool_call(两种形态都接受,内部归一化):
{"decision": "block", "reason": "Forbidden: rm -rf"} // Claude-Code 风格
{"action": "block", "message": "Forbidden: rm -rf"} // Hermes 规范
// 给 pre_llm_call 注入上下文:
{"context": "Today is Friday, 2026-04-17"}
// 在 verify 门让 Agent 继续(pre_verify);两种形态都接受:
{"action": "continue", "message": "Run the formatter, then finish."}
{"decision": "block", "reason": "Run the formatter, then finish."}
// 静默 no-op - 任何空/不匹配输出都行
JSON 畸形、非零退出码、超时都只记警告,绝不中止 Agent 循环。
代码示例
1. 每次写后自动格式化 Python 文件
# ~/.hermes/config.yaml
hooks:
post_tool_call:
- matcher: "write_file|patch"
command: "~/.hermes/agent-hooks/auto-format.sh"
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/auto-format.sh
payload="$(cat -)"
path=$(echo "$payload" | jq -r '.tool_input.path // empty')
[[ "$path" == *.py ]] && command -v black >/dev/null && black "$path" 2>/dev/null
printf '{}\n'
Agent 在上下文里看到的文件不会自动重读—格式化只影响磁盘上的文件。后续 read_file 调用会拿到格式化后的版本。
2. 拦截破坏性 terminal 命令
hooks:
pre_tool_call:
- matcher: "terminal"
command: "~/.hermes/agent-hooks/block-rm-rf.sh"
timeout: 5
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/block-rm-rf.sh
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'
fi
3. 每轮注入 git status(等价于 Claude-Code 的 UserPromptSubmit)
hooks:
pre_llm_call:
- command: "~/.hermes/agent-hooks/inject-cwd-context.sh"
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/inject-cwd-context.sh
cat - >/dev/null # 丢弃 stdin payload
if status=$(git status --porcelain 2>/dev/null) && [[ -n "$status" ]]; then
jq --null-input --arg s "$status" \
'{context: ("Uncommitted changes in cwd:\n" + $s)}'
else
printf '{}\n'
fi
NoteClaude Code 的
UserPromptSubmit事件在 Hermes 里不是单独事件—pre_llm_call在同一个地方触发且已支持上下文注入。在这用它。
4. 记每个子 Agent 完成
hooks:
subagent_stop:
- command: "~/.hermes/agent-hooks/log-orchestration.sh"
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/log-orchestration.sh
log=~/.hermes/logs/orchestration.log
jq -c '{ts: now, parent: .session_id, extra: .extra}' < /dev/stdin >> "$log"
printf '{}\n'
同意模型
每个唯一的 (event, command) 对首次被 Hermes 看到时会提示用户批准,决定持久化到 ~/.hermes/shell-hooks-allowlist.json。后续运行(CLI 或 Gateway)跳过提示。
三个逃生舱绕过交互提示—任一即可:
- CLI 的
--accept-hooks标志(如hermes --accept-hooks chat) HERMES_ACCEPT_HOOKS=1环境变量cli-config.yaml里hooks_auto_accept: true
非 TTY 运行(Gateway、cron、CI)需要三者之一—否则任何新加的 hook 静默不注册并记警告。
Warning脚本编辑被静默信任。allowlist 按精确命令字符串 key,不是脚本哈希,所以编辑磁盘上的脚本不会让同意失效。
hermes hooks doctor会标出 mtime 漂移,让你发现编辑、决定是否重新批准。
手动白名单(非 TTY 或服务账号部署用)的文件是 ~/.hermes/shell-hooks-allowlist.json,格式是 approvals 数组:
{
"approvals": [
{
"event": "post_llm_call",
"command": "/home/hermes/.hermes/hooks/my-hook.py"
}
]
}
command 字符串必须和配置的 hook command 精确匹配。用 hermes hooks list 验证手动条目。
hermes hooks CLI
| 命令 | 作用 |
|---|---|
hermes hooks list | 导出配置的 hook,带 matcher、timeout、同意状态 |
hermes hooks test <event> [--for-tool X] [--payload-file F] | 用合成 payload 触发每个匹配 hook,打印解析后的响应 |
hermes hooks revoke <command> | 移除每个匹配 <command> 的 allowlist 条目(下次重启生效) |
hermes hooks doctor | 为每个配置的 hook:检查执行位、allowlist 状态、mtime 漂移、JSON 输出有效性、粗略执行时间 |
安全
Shell hook 用你的完整用户凭据跑—和 cron 条目或 shell alias 同一个信任边界。把 config.yaml 里的 hooks: 块当特权配置对待:
- 只引用你写的或完整审过的脚本
- 脚本放在
~/.hermes/agent-hooks/里,路径好审计 - 拉了共享配置后重跑
hermes hooks doctor,在新 hook 注册前发现它们 - 配置跨团队版本控制时,审改
hooks:节的 PR 像审 CI 配置一样
顺序与优先级
Python 插件 hook 和 shell hook 都走同一个 invoke_hook() 分发器。Python 插件先注册(discover_and_load()),shell hook 后注册(register_from_config()),所以 Python pre_tool_call block 决定在平局时优先。第一个有效 block 赢—聚合器在任何回调产出带非空 message 的 {"action": "block", "message": str} 时立即返回。
常见坑
检查点没开就指望能回滚。默认关闭。要用了显式开 --checkpoints 或配置 enabled: true。
回滚前不 diff。挑错检查点越滚越乱。先 /rollback diff <N> 看清楚。
Gateway hook 指望在 CLI 也跑。Gateway hook 只在 Gateway 触发,CLI 不加载。要到处跑用 Plugin 或 Shell hook。
hook 抛异常怕崩 Agent。三套 hook 系统都非阻塞,错误被捕获记日志。但你的逻辑错了就静默失效,写 hook 时要自己测。
shell hook 脚本编辑后以为要重新批准。allowlist 按命令字符串 key,编辑脚本不失效。但这意味着别人改了你的脚本你也得不到提示—定期 hermes hooks doctor 看 mtime 漂移。
非 TTY 部署忘了加 --accept-hooks。Gateway、cron、CI 里新加的 hook 静默不注册。设 HERMES_ACCEPT_HOOKS=1 或 hooks_auto_accept: true。
pre_verify hook 不幂等。每次 nudge 后再触发,不靠 attempt 判断就一路推到 max_verify_nudges(默认 3)上限。