02-Hook 事件驱动安全机制
Hook 事件驱动安全机制
学习目标
本章将深入分析 Claude Code 中 Hook 事件的具体落地实现,通过 hookify、security-guidance 和 ralph-wiggum 三个官方插件,掌握:
- PreToolUse、PostToolUse、Stop、UserPromptSubmit 四种事件的具体实现
- Block、Warn、Intercept 三种决策模式的代码实现
- Fail-open 原则在 Python 和 Bash Hook 脚本中的落地
- 会话级状态管理和去重机制
前置知识
本章涉及 Hook 事件的通用原理,建议先阅读:
下文假设你已理解 Hook 系统的基础概念,直接聚焦 Claude Code 的具体实现。
项目实践
Hook 注册格式
Claude Code 使用 hooks/hooks.json 注册事件处理器。以 security-guidance 插件为例:
{ "hooks": { "PreToolUse": [ { "matcher": "Edit|Write|MultiEdit", "hooks": [ { "type": "command", "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/security_reminder_hook.py" } ] } ] }}Matcher 第一层过滤:只有 Edit、Write、MultiEdit 三个工具的调用才会触发安全 Hook,其他工具(如 Read、Grep)不触发,减少不必要的开销。
security-guidance:PreToolUse 安全拦截
这是 Claude Code 仓库中最简洁的安全 Hook 实现,仅 2 个文件:
输入输出协议:
- 输入:JSON 通过 stdin 传入,包含
session_id、tool_name、tool_input - 输出:用户可见的警告消息通过 stderr 输出
- 退出码:
exit 0= 放行,exit 2= 阻止
9 类安全模式监控:
| 规则 | 类型 | 触发条件 |
|---|---|---|
github_actions_workflow | 路径匹配 | .github/workflows/*.yml 中的 ${{ github.event.* }} |
child_process_exec | 内容匹配 | child_process.exec、exec( |
eval_injection | 内容匹配 | eval( |
new_function_injection | 内容匹配 | new Function |
react_dangerously_set_html | 内容匹配 | dangerouslySetInnerHTML |
document_write_xss | 内容匹配 | document.write |
innerHTML_xss | 内容匹配 | .innerHTML = |
pickle_deserialization | 内容匹配 | pickle |
os_system_injection | 内容匹配 | os.system |
会话级去重:每个 file_path-ruleName 组合在一个会话内只警告一次。状态存储在 ~/.claude/security_warnings_state_<session_id>.json 中。
hookify:四事件全覆盖
hookify 是组件最丰富的 Hook 插件,注册了四种事件:
{ "hooks": { "PreToolUse": [{"type": "command", "command": "python3 .../pretooluse.py"}], "PostToolUse": [{"type": "command", "command": "python3 .../posttooluse.py"}], "Stop": [{"type": "command", "command": "python3 .../stop.py"}], "UserPromptSubmit": [{"type": "command", "command": "python3 .../userpromptsubmit.py"}] }}核心设计特征:
- 统一入口模式:每个 Hook 脚本遵循相同模式:读 stdin → 加载规则 → 评估 → 输出 JSON →
exit 0 - 规则引擎分离:
core/rule_engine.py是独立的规则评估引擎,与事件类型解耦 - 配置加载器:
core/config_loader.py从.claude/hookify.*.local.md文件动态加载规则,无需重启 - 正则缓存:
@lru_cache(maxsize=128)优化正则匹配性能
规则文件格式:
---name: dangerous-rmenabled: trueevent: bashpattern: rm\s+-rf\s+/action: block---
阻止删除根目录及子目录的命令。规则文件存储在 .claude/ 目录中,被 .gitignore 排除(因为包含团队特定的规则)。
ralph-wiggum:Stop Hook 控制会话生命周期
这是 Claude Code 中最创新的 Hook 用法——拦截会话退出,实现迭代循环开发。
核心机制:
/ralph-loop <prompt>命令创建状态文件.claude/ralph-loop.local.md- 状态文件使用 YAML frontmatter 存储迭代计数:
---active: trueiteration: 1max_iterations: 50completion_promise: "COMPLETE"started_at: "2026-05-24T12:00:00Z"---
原始任务描述...- Stop Hook 在每次 Claude 尝试退出时触发:
- 读取状态文件,解析迭代计数
- 检查
<promise>标签是否匹配完成承诺 - 如果未完成:输出
{"decision": "block", "reason": "..."}阻止退出 - 递增迭代计数,写入新的系统消息
关键 Bash 细节:使用 = 而非 == 做精确字符串比较(== 在 [[ ]] 中执行 glob 匹配,* 字符会导致误判)。
自引用循环机制:
Claude 的之前工作保留在磁盘文件中,每次新迭代 Claude 读取自己上一轮修改的文件,形成间接的自我引用反馈循环。
Fail-open 原则落地
所有 Hook 脚本在错误路径上都以 exit 0 结束:
# hookify 的 Python Hook 脚本try: input_data = json.loads(sys.stdin.read()) rules = load_rules(event=...) result = RuleEngine().evaluate_rules(rules, input_data) print(json.dumps(result))except Exception: pass # 静默失败sys.exit(0) # 始终放行# ralph-wiggum 的 Bash Stop Hookstate_file=".claude/ralph-loop.local.md"[ ! -f "$state_file" ] && exit 0 # 状态文件不存在 → 放行陷阱与对策
| 陷阱 | 表现 | 对策 |
|---|---|---|
Hook 脚本 exit 1 | 操作被意外阻止 | 所有错误路径必须 exit 0,业务决策通过 JSON 输出而非退出码 |
Matcher 只覆盖 Edit 未覆盖 Write | 安全规则通过 Write 工具绕过 | 审计所有可写工具,确保 matcher 覆盖完整 |
Bash == 在 [[ ]] 中做 glob 匹配 | COMPLETE* 匹配 COMPLETE 以外的字符串 | 使用 = 做精确字符串比较 |
| 正则缓存命中过期规则 | 修改规则后仍匹配旧模式 | 重启 Claude Code 会话以清理缓存 |
| 状态文件 30 天未清理 | 磁盘空间膨胀 | hookify 以 10% 概率自动清理过期状态文件 |
设计取舍
为什么 Hook 用 Python/Bash 而非内置 DSL?
Python/Bash 可以访问完整的操作系统能力——文件系统、网络、外部进程。内置 DSL 虽然更安全,但无法实现复杂的安全检查(如解析 JSONL transcript、正则匹配文件内容)。
为什么安全规则嵌入 Hook 而非独立系统?
复用现有的 Hook 基础设施,避免额外的部署和维护成本。同时,Hook 的 Fail-open 原则确保安全规则失效时不会卡住正常操作。
代价:Hook 脚本的执行是外部进程调用,有启动开销(Python 解释器加载约 50-100ms)。对频繁触发的 Hook(如每次 Bash 命令前),这可能累积为可感知的延迟。
参考来源
- Claude Code Hooks 文档
- 源码:
plugins/hookify/hooks/*.py - 源码:
plugins/security-guidance/hooks/security_reminder_hook.py - 源码:
plugins/ralph-wiggum/hooks/stop-hook.sh