跳转到内容

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 第一层过滤:只有 EditWriteMultiEdit 三个工具的调用才会触发安全 Hook,其他工具(如 Read、Grep)不触发,减少不必要的开销。

security-guidance:PreToolUse 安全拦截

这是 Claude Code 仓库中最简洁的安全 Hook 实现,仅 2 个文件:

输入输出协议

  • 输入:JSON 通过 stdin 传入,包含 session_idtool_nametool_input
  • 输出:用户可见的警告消息通过 stderr 输出
  • 退出码:exit 0 = 放行,exit 2 = 阻止

9 类安全模式监控

规则类型触发条件
github_actions_workflow路径匹配.github/workflows/*.yml 中的 ${{ github.event.* }}
child_process_exec内容匹配child_process.execexec(
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"}]
}
}

核心设计特征

  1. 统一入口模式:每个 Hook 脚本遵循相同模式:读 stdin → 加载规则 → 评估 → 输出 JSON → exit 0
  2. 规则引擎分离core/rule_engine.py 是独立的规则评估引擎,与事件类型解耦
  3. 配置加载器core/config_loader.py.claude/hookify.*.local.md 文件动态加载规则,无需重启
  4. 正则缓存@lru_cache(maxsize=128) 优化正则匹配性能

规则文件格式

---
name: dangerous-rm
enabled: true
event: bash
pattern: rm\s+-rf\s+/
action: block
---
阻止删除根目录及子目录的命令。

规则文件存储在 .claude/ 目录中,被 .gitignore 排除(因为包含团队特定的规则)。

ralph-wiggum:Stop Hook 控制会话生命周期

这是 Claude Code 中最创新的 Hook 用法——拦截会话退出,实现迭代循环开发。

核心机制

  1. /ralph-loop <prompt> 命令创建状态文件 .claude/ralph-loop.local.md
  2. 状态文件使用 YAML frontmatter 存储迭代计数:
---
active: true
iteration: 1
max_iterations: 50
completion_promise: "COMPLETE"
started_at: "2026-05-24T12:00:00Z"
---
原始任务描述...
  1. 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) # 始终放行
Terminal window
# ralph-wiggum 的 Bash Stop Hook
state_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