《Claude Code 工程化实战》第 17 讲 Hooks 事件驱动自动化

📌 本讲摘要

前 16 讲讲了 SubAgent + Skills + Commands 三种能力扩展机制、本讲进入事件驱动------Hooks:在 Claude 执行工具前后插入自定义检查、做硬约束。核心是 4 类事件(PreToolUse 工具执行前 / PostToolUse 工具执行后 / Stop 主代理停止 / SubAgentStop 子代理停止)+ 两类应用(安全三件套:拦截 / 保护 / 审计;质量三件套:格式化 / Lint / 测试)。Hooks 是 4 种扩展机制中最底层的------它不描述做什么、而是在工具被调用的瞬间做检查。

本讲的三条主线:

第一、Hooks 是什么?4 类事件 + 触发时机------PreToolUse 工具执行前 exit 2 拒绝 / PostToolUse 工具执行后可修改输出 / Stop 主代理停止可强制继续 / SubAgentStop 子代理停止;配置在 .claude/settings.json 的 hooks 字段。

第二、两类典型应用------安全三件套(拦截 / 保护 / 审计、纵深防御)+ 质量三件套(格式化 / Lint / 测试、代码质量门)。

第三、Hooks vs Skills vs SubAgent 边界------Hooks 拦截(硬约束、瞬间拦截)/ Skills 描述(软规范、LLM 看 description)/ SubAgent 隔离(独立上下文);3 者的工程定位:Hooks 是门卫,Skills 是能力,SubAgent 是角色。

学完本讲、你应该能用 4 类 Hook 事件做安全拦截 / 质量门 / 审计日志、能在 .claude/settings.json 配出完整的 Hook 流水线(PreToolUse 拦危险 + PostToolUse 自动格式化 + Stop 检查未提交)、能区分 Hooks / Skills / SubAgent / Commands 4 种扩展机制的工程定位。

📖 详细内容

Hooks 是什么?4 类事件 + 触发时机

前 16 讲我们学了 SubAgent(独立上下文角色)/ Skills(自动发现能力)/ Commands(用户主动命令)3 种扩展机制。本讲进入第 4 种------Hooks、这是最底层的扩展机制、直接在工具被调用的瞬间做检查。

Hooks 的核心思想:Claude Code 在执行工具(Bash / Read / Edit / Write / Grep / Glob)的关键时间点、触发事件、你可以在 .claude/settings.json 里配置 Hook 脚本、在事件触发时执行自定义逻辑(拦截 / 记录 / 修改输出)。

4 类 Hook 事件

事件 1:PreToolUse(工具执行前)------Claude 即将调起某个工具时触发、Hook 脚本可以检查工具参数 / 命令、决定是否放行。退出码 0 = 放行、退出码 2 = 拒绝(输出到 stderr 的内容会作为拒绝原因返回给 Claude)。典型用途:拦截危险命令(rm -rf / curl | sh)。

事件 2:PostToolUse(工具执行后)------工具执行完成后触发、Hook 脚本可以读取工具输出 / 决定是否修改。退出码 0 = 通过、退出码 2 = 阻断(但工具已经执行)。典型用途:自动格式化(写完 Python 跑 ruff format)/ 审计日志(记录所有 Bash 命令)。

事件 3:Stop(主代理停止)------主代理判断任务完成、准备停止时触发、Hook 脚本可以检查是否真的完成(未提交改动 / 测试未跑)。退出码 0 = 允许停止、退出码 2 = 强制继续(Claude 收到后继续执行)。典型用途:提醒未提交的改动。

事件 4:SubAgentStop(子代理停止)------子代理完成时被触发、类似 Stop 但针对子代理。典型用途:子代理完成后自动汇总报告到主对话。

4 类事件的出场顺序是 PreToolUse → 工具执行 → PostToolUse → (返回结果)→ 重复 → (主代理判断完成)→ Stop / SubAgentStop。

配置位置:.claude/settings.json 的 hooks 字段

所有 Hook 都配置在 .claude/settings.json 里、格式:

复制代码
// .claude/settings.json 的 hooks 字段格式
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {"type": "command", "command": "bash .claude/hooks/deny-pipe-exec.sh"}
        ]
      }
    ],
    "PostToolUse": [...],
    "Stop": [...],
    "SubAgentStop": [...]
  }
}

matcher 字段指定哪个工具触发------比如"Bash"只对 Bash 工具触发、"Edit|Write"对编辑类工具触发、"*"对所有工具触发。

事件 触发时机 退出码语义 典型用途 配置复杂度
PreToolUse 工具执行前 0 = 放行、2 = 拒绝 拦截危险命令 / 拦截改 lockfile 低(读 JSON + grep + exit)
PostToolUse 工具执行后 0 = 通过、2 = 阻断(已执行) 自动格式化 / 审计日志 低(读 JSON + 跑命令 + 写日志)
Stop 主代理停止 0 = 允许、2 = 强制继续 检查未提交 / 检查测试通过 中(需要查询 git 状态 / 测试结果)
SubAgentStop 子代理停止 0 = 允许、2 = 强制继续 子代理完成后自动汇总报告 中(类似 Stop、针对子代理)

安全三件套:拦截 + 保护 + 审计

Hook 的第一类典型应用是安全三件套------拦截 / 保护 / 审计、3 件套叠加形成纵深防御。

件套 1:拦截(PreToolUse 拦危险命令)

PreToolUse 触发、Hook 脚本检查命令、危险命令 exit 2 拒绝。典型拦截 5 类命令:

  1. rm -rf / rm -fr(删除文件/目录)--- 误用会丢失数据。

  2. curl | sh / curl | bash / wget | sh(下载并执行)--- 远程代码执行风险。

  3. git push --force(强制推送)--- 会覆盖远程历史。

  4. chmod 777(放开所有权限)--- 安全风险。

  5. dd(底层磁盘操作)--- 误用会覆盖磁盘。

Hook 脚本用 grep 匹配这些模式、匹配上就 exit 2,stderr 输出"❌ 拒绝:命令被禁止、原因:..."。

件套 2:保护(Stop 时检查未提交改动)

Stop 触发、Hook 脚本跑 git status 看是否有未提交的改动。如果有、exit 2 强制 Claude 继续、提醒用户还有未提交的改动、是否需要 commit?。

保护机制防止Claude 完成任务后没提醒用户 commit、用户就关闭了会话、改动丢失。

件套 3:审计(PostToolUse 记录所有 Bash 命令)

PostToolUse 触发、Hook 脚本把所有 Bash 命令记录到 .claude/audit/.log、包含时间戳 / 用户 / 命令 / 退出码 / 输出摘要。审计日志可以:

• 事后追溯今天跑了哪些命令。

• 安全事件调查什么时候改了权限 / 什么时候删了文件。

• 团队规范审计团队成员用 Claude 时跑了哪些高风险操作。

3 件套叠加形成纵深防御:

• 拦截层(PreToolUse)------危险命令根本进不来。

• 保护层(Stop)------重要操作不会静默完成。

• 审计层(PostToolUse)------所有操作有据可查。

3 层叠加、即使某一层失效、其他层也兜底。这是防御性编程在 AI Agent 时代的应用。

质量三件套:格式化 + Lint + 测试

Hook 的第二类典型应用是质量三件套------格式化 / Lint / 测试、把代码质量门从人审自动化成机器审。

件套 1:自动格式化(PostToolUse 写完代码立即跑 ruff format)

PostToolUse 触发(matcher:"Edit|Write"),Hook 脚本检查刚写的文件是不是 Python、是就跑 ruff format <file> 自动格式化。Claude 写完代码、机器立即格式化、不用人手动跑。

件套价值:把团队代码风格从团队成员自觉变成机器强制、任何 PR 都不会出现格式问题。

件套 2:Lint 检查(PreToolUse 检查 Edit 工具是否修改了不该改的文件)

PreToolUse 触发(matcher:"Edit|Write"),Hook 脚本检查文件路径、如果是不该改的文件(比如 lock 文件 / 配置文件)就 exit 2 拒绝。

典型:防止 Claude 误改 package-lock.json / yarn.lock / requirements.txt(这些文件应该用 npm install / pip install 改、不应该直接编辑)。

件套 3:测试(Stop 时检查所有测试是否通过)

Stop 触发、Hook 脚本跑 pytest tests/ -q 看是否全过。如果有失败、exit 2 强制 Claude 继续、提醒测试未通过、需要修复。

件套价值:把测试通过从PR review 的人工检查变成机器强制门、任何 PR 提交前都过测试。

3 件套叠加形成质量门:

• 写代码时自动格式化(避免风格问题)。

• 改文件时 Lint 检查(避免改错文件)。

• 完成任务时测试验证(避免破坏现有功能)。

质量门 + 安全门、组成完整的Claude Code 防御体系------这是工程化实战的核心。

维度 安全三件套 质量三件套
触发事件 PreToolUse(拦截) + Stop(保护) + PostToolUse(审计) PostToolUse(格式化) + PreToolUse(Lint) + Stop(测试)
拦截 vs 改进 拦截为主(拒绝危险操作) 改进为主(自动格式化 / 提醒)
静默 vs 可见 静默(用户看不到拦截过程) 可见(用户看到格式化 / 测试输出)
实施成本 低(grep + exit 2) 中(需要 ruff / pytest / git)
用户感知 低(很少触发、触发时拒绝) 中(频繁触发、但用户不反感)

Hooks 配置实战:settings.json + 3 个 Hook 脚本

完整的 Hook 配置需要 2 步:.claude/settings.json 配置事件 + 钩子命令;.claude/hooks/ 目录放 3 个 Hook 脚本。

step 1:.claude/settings.json 完整配置

复制代码
// .claude/settings.json(完整 Hook 配置)
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {"type": "command", "command": "bash .claude/hooks/deny-pipe-exec.sh"},
          {"type": "command", "command": "bash .claude/hooks/deny-extra-dangerous.sh"}
        ]
      },
      {
        "matcher": "Edit|Write",
        "hooks": [
          {"type": "command", "command": "bash .claude/hooks/deny-edit-lockfile.sh"}
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {"type": "command", "command": "bash .claude/hooks/auto-format.sh"},
          {"type": "command", "command": "bash .claude/hooks/audit-log.sh"}
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {"type": "command", "command": "bash .claude/hooks/check-uncommitted.sh"},
          {"type": "command", "command": "bash .claude/hooks/check-tests.sh"}
        ]
      }
    ]
  }
}

这个配置覆盖了 3 类事件(PreToolUse / PostToolUse / Stop)、每个事件配 1-2 个 Hook 脚本、形成防御 + 质量 + 审计完整链路。

step 2:.claude/hooks/ 目录放 3 个核心 Hook 脚本

3 个核心脚本:

deny-pipe-exec.sh(PreToolUse 拦截 pipe 执行)

auto-format.sh(PostToolUse 自动格式化)

check-uncommitted.sh(Stop 检查未提交)

每个脚本都是一个独立的 Bash 脚本、可以从 stdin 读 JSON(包含工具名 / 参数 / 命令)、从 stdout 输出结果、退出码 0/2 表示通过/拒绝。

3 个 Hook 实战案例

案例 1:危险命令拦截(5 种)

PreToolUse Hook 拦截 5 类危险命令:rm -rf / curl | sh / git push --force / chmod 777 / dd。实现:用 grep -E 匹配命令、匹配上 exit 2 + stderr 输出拒绝原因。

完整实现见后面5 类危险命令拦截规则代码块。

案例 2:自动格式化

PostToolUse Hook 自动跑 ruff format(写完 Python 立即格式化)。实现:从 stdin 读 JSON(包含 file_path)、如果是 .py 文件就跑 ruff format <file>

自动格式化的优势:Claude 写代码时不需要记住 PEP 8、机器自动保证格式一致。

案例 3:审计日志

PostToolUse Hook 记录所有 Bash 命令到 .claude/audit/.log。实现:从 stdin 读 JSON(包含 command)、追加一行 [timestamp] command= exit=`` 到日志文件。 ``

审计日志的价值:事后追溯 / 安全事件调查 / 团队规范审计。

Hooks vs Skills vs SubAgent 边界

第 14 讲说过4 种扩展机制------Commands(用户主动)/ SubAgent(角色层)/ Skills(能力层)/ Hook(机制层)。本讲专门讲 Hook 的工程定位。

3 种机制的工程定位

Hooks:门卫------在工具被调用的瞬间做检查,硬约束,瞬间拦截。负责不能做的事和必须做的事。

Skills:能力------LLM 推理该用就用,软规范,渐进披露。负责该做的事。

SubAgent:角色------独立上下文,@name 显式调。负责复杂任务的隔离执行。

3 者的工作层次不同:Hook 在工具层(瞬时检查),Skill 在能力层(LLM 推理),SubAgent 在角色层(独立上下文)。

3 者的协同

典型协同场景:危险操作 Skill(如 rollback Skill)需要用户主动 + 双保险------Skill 加 disable-model-invocation: true(LLM 看不到)+ Hook 拦截危险命令(双保险)。3 者形成用户主动调 Skill + Skill 调用时 Hook 兜底的两层防御。

典型反协同:不要用 Hook 替代 Skill。Hook 只能拦截/记录,不能做复杂任务------比如代码审查,Hook 只能检查是否调用了 reviewer,不能实际跑审查。实际审查用 Skill 或 SubAgent。

3 者的边界判断:这件事是瞬间检查吗?是 → Hook。这件事是LLM 推理该用就用吗?是 → Skill。这件事需要独立上下文吗?是 → SubAgent。这件事是用户主动调吗?是 → Command。

🛠️ 实战代码

📄 第 17 讲配套:3 个 Hook 脚本完整实现(deny-pipe-exec.sh / auto-format.sh / check-uncommitted.sh)

复制代码
#!/usr/bin/env bash
# .claude/hooks/deny-pipe-exec.sh
# 拦截 curl | sh / wget | bash 等 pipe 执行
set -e

# 从 stdin 读 JSON(Claude Code 注入的 hook 输入)
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')

# 拦截 pipe 执行
if echo "$COMMAND" | grep -qE 'curl\s+.*\|\s*(sh|bash)|wget\s+.*\|\s*(sh|bash)'; then
  echo "❌ 拒绝:pipe 执行被禁止(curl/wget | sh/bash 是远程代码执行风险)" >&2
  exit 2
fi

exit 0

#!/usr/bin/env bash
# .claude/hooks/auto-format.sh
# 写完 Python 立即 ruff format
set -e

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')

# 只对 .py 文件做格式化
if [[ "$FILE_PATH" == *.py ]]; then
  if command -v ruff > /dev/null; then
    ruff format "$FILE_PATH" 2>/dev/null || true
    echo "✅ 已自动格式化: $FILE_PATH"
  fi
fi

exit 0

#!/usr/bin/env bash
# .claude/hooks/check-uncommitted.sh
# Stop 时检查是否有未提交改动
set -e

# 在 git 仓库内才检查
if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
  exit 0
fi

# 检查未提交改动
if ! git diff --quiet HEAD 2>/dev/null || [ -n "$(git status --porcelain)" ]; then
  echo "⚠️ 还有未提交的改动,记得 git commit" >&2
  echo "" >&2
  git status --short >&2
  exit 2  # exit 2 = 强制 Claude 继续
fi

exit 0

📄 第 17 讲配套:5 类危险命令拦截规则(rm/curl|sh/git push --force/chmod 777/dd)

复制代码
#!/usr/bin/env bash
# .claude/hooks/deny-extra-dangerous.sh
# 拦截 5 类危险命令
set -e

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')

# === 规则 1:rm -rf / rm -fr(递归删除) ===
if echo "$COMMAND" | grep -qE '\brm\s+(-[a-zA-Z]*[rf][a-zA-Z]*\s+|-[a-zA-Z]*[r][a-zA-Z]*f|-[a-zA-Z]*[f][a-zA-Z]*r)\s+'; then
  echo "❌ 拒绝:rm -rf 被禁止(防止误删数据,改用 rm -i 交互式删除)" >&2
  exit 2
fi

# === 规则 2:curl | sh / curl | bash / wget | sh / wget | bash ===
if echo "$COMMAND" | grep -qE '(curl|wget)\s+.*\|\s*(sh|bash|sudo)'; then
  echo "❌ 拒绝:pipe 执行被禁止(curl | sh 是远程代码执行风险)" >&2
  exit 2
fi

# === 规则 3:git push --force / git push -f(强制推送) ===
if echo "$COMMAND" | grep -qE 'git\s+push\s+.*--force|git\s+push\s+.*-f\b'; then
  echo "❌ 拒绝:git push --force 被禁止(会覆盖远程历史,改用 git push --force-with-lease)" >&2
  exit 2
fi

# === 规则 4:chmod 777(放开所有权限) ===
if echo "$COMMAND" | grep -qE 'chmod\s+777|chmod\s+-R\s+777|chmod\s+-R\s+0777'; then
  echo "❌ 拒绝:chmod 777 被禁止(放开所有权限有安全风险,改用 644 / 755)" >&2
  exit 2
fi

# === 规则 5:dd(底层磁盘操作) ===
if echo "$COMMAND" | grep -qE '\bdd\s+'; then
  echo "❌ 拒绝:dd 命令被禁止(底层磁盘操作,误用会覆盖磁盘,改用更安全的工具)" >&2
  exit 2
fi

exit 0

📄 第 17 讲配套:审计日志的 Bash 记录实现(.claude/audit/.log)

复制代码
#!/usr/bin/env bash
# .claude/hooks/audit-log.sh
# 记录所有 Bash 命令到 .claude/audit/<date>.log
set -e

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
EXIT_CODE=$(echo "$INPUT" | jq -r '.tool_result.exit_code // "?"')
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

# 创建 audit 目录
mkdir -p .claude/audit

# 追加日志(每天一个文件)
LOG_FILE=".claude/audit/$(date '+%Y-%m-%d').log"
echo "[$TIMESTAMP] exit=$EXIT_CODE command=$COMMAND" >> "$LOG_FILE"

exit 0

# 审计日志示例(.claude/audit/2026-06-02.log)
[2026-06-02 10:15:23] exit=0 command=git status
[2026-06-02 10:16:45] exit=0 command=git diff --stat
[2026-06-02 10:18:12] exit=0 command=pytest tests/ -q
[2026-06-02 10:25:33] exit=0 command=ruff format src/api.py
[2026-06-02 10:30:18] exit=2 command=rm -rf node_modules/
[2026-06-02 10:30:18] ❌ 拒绝:rm -rf 被禁止(...)

# 审计日志的 3 个用途:
# 1. 事后追溯:今天跑了哪些命令
# 2. 安全事件调查:什么时候改了权限 / 什么时候删了文件
# 3. 团队规范审计:团队成员用 Claude 时跑了哪些高风险操作

⚠️ 常见坑

⚠️ Hook 脚本写得太慢(超过 1 秒、影响 Claude 响应速度)

Hook 性能陷阱:PostToolUse Hook 每次 Claude 调 Edit/Write 都会触发、如果 Hook 跑得慢(比如自动跑 pylint 而不只是 ruff format、要 5-10 秒),Claude 整体响应速度会被拖慢。修正:Hook 脚本要轻------用 grep 匹配比用 python -c 跑复杂逻辑快;缓存常用结果(比如 ruff 检查过的文件不要重复检查);长时间操作(跑完整测试套件)改用 Stop 触发而不是 PreToolUse。判断标准:HW 脚本 > 1 秒 = 太慢、优化。

⚠️ Hook exit 2 但没给清晰错误信息(用户不知道为啥被拒)

"拒绝不解释"的体验陷阱:Hook exit 2 但 stderr 只输出"❌ 拒绝"没说原因、Claude 收到拒绝信号后不知道怎么办、用户也不知道为啥被拒。修正:exit 2 时、stderr 必须输出清晰的拒绝原因------"❌ 拒绝:rm -rf 被禁止、原因:防止误删数据、改用 rm -i 交互式删除"。这样 Claude 收到后能告诉用户"我被拒绝的原因、你可以用 rm -i 替代"、形成完整反馈链。判断标准:Hook 拒绝时 stderr 输出的信息、用户能直接看懂原因?能、正确;不能、补全。

⚠️ Hook 拦截了合法操作(比如 "rm /tmp/test" 是合法清理、被误拦)

误拦的过度防御:Hook 规则写得太宽------所有 rm 命令都拦、结果 rm /tmp/test.txt(合法的临时文件清理)也被拦。修正:HW 规则要精准------只拦真正危险的模式(rm -rf / rm -fr + 关键目录)、不拦普通 rm。规则写好后、在测试环境跑一组应该被拦的命令和应该被放过的命令、确保两个集合都正确。判断标准:HW 误拦率 > 5%?调规则。

⚠️ Hook 只配置没测试(只在生产配置、没在测试环境跑过)

配了但没测的风险:配置 Hook 后没在测试环境验证、直接上生产、结果某天发现HW 没生效(可能是路径写错 / 权限问题 / jq 没装)、危险命令没被拦下来。修正:配置后立即在测试环境跑5 个应该被拦的命令和5 个应该被放过的命令、确认两个集合都正确。再用 Stop HW 的未提交检查故意制造未提交改动、确认 HW 能强制 Claude 继续。判断标准:HW 配置后、你在测试环境跑过 >10 个测试用例吗?没有、补测。

💡 一句话备忘

Hooks 是 4 种扩展机制中最底层的------在工具被调用的瞬间做硬约束(PreToolUse 拦截 / PostToolUse 记录 / Stop 强制继续)、形成安全三件套(拦截 + 保护 + 审计)+ 质量三件套(格式化 + Lint + 测试)的纵深防御。

相关推荐
老谷子.AI原始技术3 小时前
《Claude Code 工程化实战》第 18 讲 Hooks 高级模式与工程实践
claude code
西游音月7 小时前
Windows环境下的WSL+Claude Code安装配置
windows·大模型·ai编程·wsl·claude code
月走乂山2 天前
Anything Analyzer MCP 401 Unauthorized 故障排查
electron·故障排查·mcp·claude code·anything analyzer
码哥字节2 天前
为什么AI生成的单测,覆盖率看起来很高但上线还是出Bug?
测试覆盖率·claude code·ai单元测试
huy1n92 天前
Ubuntu 22.04部署Claude Code并接入DeepSeek-V4模型完整教程
linux·ubuntu·deepseek·claude code
仙逆GPT2 天前
Claude Code上下文满了怎么办?compact、clear和resume怎么用
大语言模型·ai编程·上下文·claude code·compact
Maynor9962 天前
AI Coding 零基础实战教程|第五部分:完整项目案例实操
java·前端·人工智能·claude code·ai coding
AITOP1002 天前
工信部发布Claude Code后门风险提示:首次对海外AI编程工具定调“危害严重“
claude code·ai编程工具·海外ai编程工具