Agent 的工作流程,本质上就是"接收指令→调用工具→返回结果"的循环。
但这个描述太简化了。真正的问题是:在工具调用之前和之后,我们有没有机会插一脚?
举几个我实际遇到的场景:
rm -rf 之前自动拦截,不要等我反应过来才后悔tsc --noEmit,而不是手动记着去做这些场景有一个共同点:它们都不是"主流程"的一部分,但少了它们,系统就是残废的。
这就是 Hook 系统 要解决的问题。
ECC 定义了 6 种 Hook 事件:
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
| SessionStart | 会话开始时 | 加载前序上下文、检测项目状态 |
| PreToolUse | 工具执行之前 | 安全检查、提醒、验证 |
| PostToolUse | 工具执行之后 | 格式化、类型检查、连续学习记录 |
| UserPromptSubmit | 用户发送消息时 | 消息预处理 |
| PreCompact | 上下文压缩之前 | 保存重要状态 |
| Stop | 模型停止响应时 | 审计检查、持久化摘要 |
每个 Hook 由三部分组成:
tool == "Bash" && tool_input.command matches "npm|pytest"minimal / standard / strict,控制 Hook 的激进程度。下面是 ECC 的一个实际 Hook 配置:
{
"PreToolUse": [
{
"matcher": "tool == \"Bash\" && tool_input.command matches \"(npm|pnpm|yarn|cargo|pytest)\"",
"hooks": [{
"type": "command",
"command": "if [ -z \"$TMUX\" ]; then echo '[Hook] 建议在 tmux 中运行长命令' >&2; fi"
}]
},
{
"matcher": "tool == \"Write\" && tool_input.file_path matches \"\\.md$\"",
"hooks": [{
"type": "command",
"command": "echo '[Hook] 非文档文件不要写 .md' >&2;"
}]
}
],
"PostToolUse": [
{
"matcher": "tool == \"Edit\" && tool_input.file_path matches \"\\.(ts|tsx)$\"",
"hooks": [{
"type": "command",
"command": "tsc --noEmit 2>&1"
}]
}
]
} 这里的关键设计是 Matcher 是可编程的——不是硬编码的"在写文件前检查",而是可以在运行时通过表达式匹配任意条件。这让 Hook 系统的扩展性远远超过简单的"前后回调"。
在 ECC 的真实使用中,PreToolUse 被大量用于安全检查。你可以在工具执行之前做:
rm -rf /、DROP TABLE、git push --force 这些危险操作自动拦截这些检查在系统层面做一次,比在每次对话中反复提醒模型要可靠得多。Hook 的保险作用在于:它不依赖模型的"自觉"。 模型可能在某次对话中被你提醒过"不要删文件",但换个场景它可能就忘了。PreToolUse 的检查是系统级的,每次都会执行。
PostToolUse 的典型用法是"写完后自动检查":
ECC 甚至有一个 Stop 阶段的 Hook,专门扫描所有被修改的文件,看有没有残留的调试代码。这个 Hook 是"阻塞模式"——检查失败会阻止整个操作被标记为完成。
ECC 的 Continuous Learning v2 系统,核心传感器就是 PostToolUse Hook。
每次工具调用完成后,Hook 记录下来"用了什么工具、做了什么、结果如何"。后台的分析器会从这些记录中提取模式,形成 Instinct(本能),最终演化为可重用的 Skill。
这个过程不需要人工干预——Hook 自动采集数据,分析器自动提炼模式,最终输出可复用的 Skill。如果你没有 Hook 系统,你就没有数据来源,连续学习就无从谈起。
| 场景 | 有 Hook 系统 | 没有 Hook 系统 |
|---|---|---|
| 执行危险命令 | PreToolUse 自动拦截 | 靠模型自觉,或者弹窗确认 |
| 写完代码检查 | PostToolUse 自动跑检查 | 手动想起来再跑 |
| 跨会话记忆 | Stop Hook 自动保存摘要 | 每次会话归零 |
| 连续学习 | PostToolUse 记录数据 | 靠人工回顾 |
| 安全审计 | 所有操作都有日志 | 没有系统化记录 |
Hook 系统的本质,是把"偶发的、靠人记着做"的事情,变成"必然的、系统自动做"的事情。
如果你的 Agent 系统没有原生的 Hook 支持,你可以在工具调用包装层实现一个简化版。
用一个 JSON 文件管理所有 Hook:
{
"preToolUse": [
{ "matcher": "terminal", "severity": "high", "action": "check_command_safety" },
{ "matcher": "write_file", "action": "check_file_extension" }
],
"postToolUse": [
{ "matcher": "terminal", "action": "record_tool_usage" },
{ "matcher": "write_file", "action": "lint_modified_file" }
],
"onStop": [
{ "action": "save_session_summary" }
]
} def execute_with_hooks(tool_name, tool_input):
# PreToolUse
for hook in registry.get("preToolUse", []):
if matches(hook["matcher"], tool_name):
execute_action(hook["action"], tool_name, tool_input)
# 实际的工具调用
result = execute_tool(tool_name, tool_input)
# PostToolUse
for hook in registry.get("postToolUse", []):
if matches(hook["matcher"], tool_name):
execute_action(hook["action"], tool_name, tool_input, result) Hook 系统的最终目标不是"加更多钩子",而是把重复的、容易遗忘的事情自动化。一个好的衡量标准是:
1. 列出你的"遗忘清单"。 有哪些事情你经常忘记做?跑测试、格式化代码、保存笔记。每个"忘记"都是一个潜在的 Hook。
2. 从最简单的 PostToolUse 开始。 不需要一开始就搭完整的 Hook 框架。先选一个你最常忘记的检查(比如改完代码跑 lint),在工具调用后自动执行它。
3. 在 PreToolUse 建一个安全门。 至少对 rm -rf、DROP TABLE、git push --force 这三个操作做拦截。这是投入产出比最高的 Hook。
今天就在你的工具调用流程中加上一条 PreToolUse 检查——拦截任何包含 rm -rf 的 terminal 命令。只需要三行代码,就能避免一次灾难性事故。
💬 评论