首页 / Hermes Agent 教程 / 检查点、回滚与 Hooks 钩子

Hermes Agent 教程

检查点、回滚与 Hooks 钩子

本教程共 25 篇 · 第 20 篇 · 更新于 2026-07-26 · 约 16 分钟阅读

Hermes AgentHermes Agent 教程检查点Checkpoint回滚rollbackHooks钩子verify-hooksshell-hooks

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_filepatch
  • 破坏性终端命令rmrmdircpinstallmvsed -itruncateddshred、输出重定向(>)、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 在背后做四件事:

  1. 验证目标 commit 在 shadow store 里存在
  2. 给当前状态拍一个回滚前快照,让你之后能「撤销这次撤销」
  3. 把工作目录里被追踪的文件恢复回去
  4. 撤销最近一轮对话,让 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 liststatus 别名
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 或按 profile enabled: 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.yamlhooks: 块指向 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:startupGateway 进程启动platforms(活跃平台名列表)
session:start新消息会话创建platform, user_id, session_id, session_key
session:end会话结束(重置前)platform, user_id, session_key
session:reset用户跑了 /new/resetplatform, user_id, session_key
agent:startAgent 开始处理消息platform, user_id, session_id, message
agent:step工具调用循环每轮迭代platform, user_id, session_id, iteration, tool_names
agent:endAgent 处理完成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:modelcommand:reset 等)。一次订阅监控所有斜杠命令。

经典模式:BOOT.md 启动清单

社区流行一个模式:在 ~/.hermes/BOOT.md 放一份 Markdown 清单,Gateway 每次启动时让 Agent 跑一遍。适合「每次启动检查昨晚 cron 失败没,有事就在 Discord 喊我」或「总结最近 24 小时 deploy.log 发到 Slack #ops」。

Hermes 不内置这个 hook,你自己照下面搭:

  1. ~/.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.
  1. 建 hook 目录 ~/.hermes/hooks/boot-md/HOOK.yaml 订阅 gateway:startuphandler.py 在后台线程起一个一次性 Agent,用 Gateway 解析出来的 model 和凭据跑 BOOT.md 内容。Agent 看到这是它的 prompt,所以你平铺直叙写就行—工具调用、shell 命令、发消息、总结文件都行。

  2. 重启 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 目录里搭,看到它具体干啥,写文件才算启用。

工作原理

  1. Gateway 启动时,HookRegistry.discover_and_load()~/.hermes/hooks/
  2. 每个含 HOOK.yaml + handler.py 的子目录被动态加载
  3. handler 按声明的事件注册
  4. 每个生命周期点,hooks.emit() 触发所有匹配 handler
  5. 任何 handler 的错误被捕获并记日志—坏 hook 永不崩 Agent
Note

Gateway 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_idapi_request_idtask_idsession_idapi_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_finalizeCLI/Gateway 拆掉活跃会话(flush、保存、统计)忽略
on_session_resetGateway 换新会话 key(如 /new/reset忽略
subagent_startdelegate_task 子 Agent 构造完、即将运行忽略
subagent_stopdelegate_task 子 Agent 退出忽略
pre_gateway_dispatchGateway 收到用户消息、auth + 派发前`{“action”: “skip"
pre_approval_request请求批准决策(含 smart 模式自动决策)忽略
post_approval_response批准决策做出(或提示超时)忽略
transform_tool_result任何工具返回后、结果交给模型前str 替换结果,None 不变
transform_terminal_outputterminal 工具内、截断/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_completekanban_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_startdelegate_task 构造完子 Agent、运行前触发,单任务或三任务批量都对每个子 Agent 触发一次。这是观察 hook,返回值不阻塞也不改子 Agent 运行。要拦委托,用 pre_tool_calldelegate_task 工具调用。

subagent_stop 在子 Agent 完成后触发,串行化在父线程上,所以你不用担心并发回调。重委托(编排者 × 5 叶子 × 嵌套深度)时这 hook 一轮触发很多次,保持回调快,把贵活推到后台队列。

transform_*:改写结果与响应

三个 transform hook 让你用经典编程改写文本,不烧额外推理 token:

  • transform_tool_result:工具返回后、结果追加到对话前。能改任何工具的结果字符串
  • transform_terminal_outputterminal 工具内、50 KB 默认截断和 ANSI 剥离前。改 shell 命令的原始 stdout/stderr
  • transform_llm_output:每轮一次,工具循环完成后、最终响应交给用户前。改助手最终文本

适用:从 web_extract 输出脱敏组织特定 PII、给长 JSON 工具响应加摘要头、给 read_file 结果注入 RAG 提示、把 delegate_task 子 Agent 报告改写成项目特定 schema、给最终响应加人格变换(海盗腔、Spongebob)。

Shell hooks:拖入脚本不用写 Python

~/.hermes/config.yamlhooks: 块里声明 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 hooksPlugin hooksGateway hooks
声明在~/.hermes/config.yamlhooks:plugin.yaml 插件里的 register()HOOK.yaml + handler.py 目录
住在~/.hermes/agent-hooks/(约定)~/.hermes/plugins/<name>/~/.hermes/hooks/<name>/
语言任意(Bash、Python、Go 二进制…)仅 Python仅 Python
跑在哪CLI + GatewayCLI + 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_callsubagent_stop、会话生命周期)的 tool_nametool_inputnullextra dict 装所有事件特定 kwargs(user_messageconversation_historychild_roleduration_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
Note

Claude 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)跳过提示。

三个逃生舱绕过交互提示—任一即可:

  1. CLI 的 --accept-hooks 标志(如 hermes --accept-hooks chat
  2. HERMES_ACCEPT_HOOKS=1 环境变量
  3. cli-config.yamlhooks_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=1hooks_auto_accept: true

pre_verify hook 不幂等。每次 nudge 后再触发,不靠 attempt 判断就一路推到 max_verify_nudges(默认 3)上限。