Skip to content

04 - Hooks 体系设计

1. 定位

Hooks 是确定性的质量闸门。与 CLAUDE.md 的"建议性"指令不同,Hooks 保证每次条件满足时执行。

与 CLAUDE.md 的本质区别(官方明确说明):

  • CLAUDE.md 指令是建议性的 — Claude 尽力遵循但不保证每次执行
  • Hooks 是确定性的 — 条件满足即保证执行,无例外

核心价值:

  • 把"必须每次无例外执行"的动作从"靠 AI 记住"变为"自动执行"
  • 减轻 CLAUDE.md 的负担(不需要写"每次编辑后运行 prettier"这类规则)
  • 经验法则:如果某条规则违反一次就会造成问题,用 Hook 而非 CLAUDE.md

2. Hook 事件:按防线目的选择

不要从事件列表出发想"我能用哪些 Hook",而是从防线需求出发想"我需要防什么"。

安全防线 — 阻止不可逆操作

  • PreToolUse(Bash)— 拦截危险命令(rm -rf、force push、sudo)
  • PreToolUse(Edit|Write)— 拦截对关键文件的修改
  • PermissionRequest — 自动审批策略、审计记录

质量防线 — 保证产出标准

  • PostToolUse(Write|Edit)— 编辑后自动格式化、文件行数检查
  • PostToolUseFailure — 工具失败后的自动诊断或重试策略
  • TaskCompleted — Agent Teams 任务完成时的质量门控

上下文防线 — 防止信息丢失

  • PreCompact — 压缩前注入保留指令,保护关键上下文
  • InstructionsLoaded — 审计 CLAUDE.md/rules 的加载情况

生命周期 — 环境管理

  • SessionStart/SessionEnd — 会话启动/结束时的环境加载和资源清理
  • SubagentStart/SubagentStop — 子代理启动前注入指令、完成后聚合结果
  • TeammateIdle — Agent Teams 成员空闲时重新分配任务
  • 其他:UserPromptSubmitStopNotificationPromptConfigChangeWorktreeCreate/WorktreeRemove

个人开发者的核心三件套:PreToolUse(安全)、PostToolUse(质量)、PreCompact(上下文保护)。其他按需启用。

完整事件列表、Matcher 语法和 JSON 输入输出格式参见 Hooks 官方参考。本文档聚焦选择判断和实际配置。

3. Hook 决策机制

Hook 通过两种方式告诉 Claude Code 该怎么做:

简单模式:退出码

退出码含义效果
0通过工具调用正常执行
1警告不阻断,但 AI 会看到 stderr 中的警告信息
2阻断阻止操作,stderr 内容反馈给 AI 指导修正

结构化模式:JSON 输出(推荐)

对于 PreToolUse 等决策类 Hook,推荐输出结构化 JSON,语义更清晰:

bash
# stdout 输出 JSON 决策
jq -n '{
  hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "deny",
    permissionDecisionReason: "Destructive command blocked by hook"
  }
}'

两种模式等效,新写的 Hook 推荐用 JSON 模式。关键点:无论哪种模式,阻断时的反馈信息不只是"拦截",更是"指导"——AI 会据此修正行为。

4. Hook 输入数据

Hook 通过 stdin 接收 JSON 格式的上下文数据:

json
// PreToolUse / PostToolUse
{
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf /tmp/test"
  }
}

// PostToolUse 额外包含
{
  "tool_name": "Edit",
  "tool_input": {
    "file_path": "/src/app.ts",
    "old_string": "...",
    "new_string": "..."
  },
  "tool_output": "File edited successfully"
}

5. 生产级 Hook 配置

5.1 完整的 settings.json hooks 配置

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{
          "type": "command",
          "command": ".claude/hooks/pre-bash-firewall.sh"
        }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{
          "type": "command",
          "command": ".claude/hooks/post-edit-format.sh"
        }]
      }
    ],
    "PreCompact": [
      {
        "matcher": "auto",
        "hooks": [{
          "type": "command",
          "command": ".claude/hooks/pre-compact-preserve.sh"
        }]
      }
    ]
  }
}

5.2 安全防火墙 — pre-bash-firewall.sh

阻断危险的 Bash 命令(使用 JSON 结构化输出):

bash
#!/usr/bin/env bash
set -euo pipefail

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

dangerous_patterns=(
  'rm -rf /'
  'git reset --hard'
  'git push.*--force'
  'git clean -f'
  'sudo '
  'chmod 777'
  'curl.*|.*sh'
  'wget.*|.*sh'
)

for pattern in "${dangerous_patterns[@]}"; do
  if echo "$cmd" | grep -qE "$pattern"; then
    jq -n --arg reason "Blocked dangerous pattern '$pattern' in: $cmd" '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: $reason
      }
    }'
    exit 0
  fi
done

exit 0

5.3 编辑后自动格式化 — post-edit-format.sh

bash
#!/usr/bin/env bash
set -euo pipefail

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

# 只处理项目内的文件
case "$file_path" in
  *.ts|*.tsx|*.js|*.jsx)
    if command -v npx &> /dev/null; then
      npx prettier --write "$file_path" 2>/dev/null || true
    fi
    ;;
  *.json)
    if command -v npx &> /dev/null; then
      npx prettier --write "$file_path" 2>/dev/null || true
    fi
    ;;
esac

exit 0

5.4 自动压缩时保留上下文 — pre-compact-preserve.sh

bash
#!/usr/bin/env bash
cat <<'EOF'
{
  "hookSpecificOutput": {
    "hookEventName": "PreCompact",
    "additionalContext": "压缩时必须保留:1. 所有已修改文件的路径 2. 当前任务进度和下一步计划 3. 测试命令和运行结果 4. 重要的架构决策 5. 失败的尝试及原因"
  }
}
EOF
exit 0

6. 运行时规范强制执行 Hooks

除了前面 3 个核心 Hook,以下 Hook 在运行时自动强制执行编码规范,不依赖 AI "记住"规则:

6.1 写入后文件行数检查 — post-write-size-guard.sh

AI 写入或编辑文件后,自动检查是否超过 300 行限制:

bash
#!/usr/bin/env bash
set -euo pipefail
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')

# 只检查 TS/TSX 文件
case "$file_path" in
  *.ts|*.tsx)
    if [ -f "$file_path" ]; then
      lines=$(wc -l < "$file_path")
      if [ "$lines" -gt 300 ]; then
        echo "WARNING: $file_path 已达 ${lines} 行,超过 300 行限制。请考虑拆分。" >&2
        # 退出码 1 = 非阻断警告,AI 会看到但不会被拦截
        exit 1
      fi
    fi
    ;;
esac
exit 0

注意:退出码是 1 不是 2。不阻断写入,但 AI 会收到警告并据此行动。

6.2 新依赖拦截 — pre-bash-dep-guard.sh

AI 执行 npm install 时,检查是否在白名单中:

bash
#!/usr/bin/env bash
set -euo pipefail
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // ""')

# 只拦截 npm install / npm add / yarn add / pnpm add
if ! echo "$cmd" | grep -qE '(npm install|npm add|yarn add|pnpm add)'; then
  exit 0
fi

# 提取包名(去掉 flags 和版本号)
pkg=$(echo "$cmd" | grep -oE '(npm install|npm add|yarn add|pnpm add)\s+\S+' | awk '{print $NF}' | sed 's/@.*//')

if [ -z "$pkg" ] || [ "$pkg" = "-D" ] || [ "$pkg" = "--save-dev" ]; then
  exit 0  # 无包名或只有 flag,放行
fi

# 检查禁止列表
banned=("moment" "jquery" "lodash")
for b in "${banned[@]}"; do
  if [ "$pkg" = "$b" ]; then
    echo "BLOCKED: $pkg 在禁止依赖列表中。请查看 docs/approved-deps.md 获取替代方案。" >&2
    exit 2
  fi
done

# 检查白名单(如果存在)
if [ -f "docs/approved-deps.md" ]; then
  if ! grep -q "$pkg" docs/approved-deps.md; then
    echo "WARNING: $pkg 不在依赖白名单中。请评估是否需要,通过后添加到 docs/approved-deps.md。" >&2
    exit 1  # 警告但不阻断
  fi
fi

exit 0

6.3 settings.json 中的完整 Hooks 配置

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {"type": "command", "command": ".claude/hooks/pre-bash-firewall.sh"},
          {"type": "command", "command": ".claude/hooks/pre-bash-dep-guard.sh"}
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {"type": "command", "command": ".claude/hooks/post-edit-format.sh"},
          {"type": "command", "command": ".claude/hooks/post-write-size-guard.sh"}
        ]
      }
    ],
    "PreCompact": [
      {
        "matcher": "auto",
        "hooks": [
          {"type": "command", "command": ".claude/hooks/pre-compact-preserve.sh"}
        ]
      }
    ]
  }
}

6.4 三层保障总览

                   脚本 check          Hooks              Skills /check
触发方式            人工/CI 运行        每次工具调用自动     人工调用
检查时机            事后审计            实时拦截/警告        按需深度检查
能做的              文件存在性           文件行数             代码语义分析
                   配置格式            危险命令             CLAUDE.md 健康度
                   行数统计            禁止依赖             测试覆盖评估
                   禁止依赖            自动格式化           架构一致性
                   .env 安全                               依赖合理性
需要 AI             否                  否                  是
执行速度            秒级                毫秒级              分钟级
阻断能力            CI 失败             退出码 2 阻断        建议性

三者互补,不重叠:

  • Hooks 管实时 — 每次编辑/命令都自动检查,零成本
  • 脚本 check 管审计 — 定期或 CI 中全面扫描,不需要 AI
  • Skill /check 管深度 — 需要 AI 理解代码语义时使用

7. 设计原则

7.1 三个必须

  1. 快速 — 每个 Hook 执行控制在 2 秒内。格式化单文件通常 < 500ms。如果 Hook 太慢会严重影响开发体验
  2. 幂等 — 重复执行不产生副作用。prettier --write 是幂等的(格式化后再格式化结果不变)
  3. 容错 — 非关键 Hook 失败不应阻断工作流。用 || true 或非 2 的退出码

7.2 不该用 Hook 做的事

  • 复杂的逻辑判断(交给 Skill 或 CLAUDE.md 规则)
  • 耗时的操作(完整测试套件不应放在 PostToolUse,应该在提交前手动运行)
  • 需要 AI 推理的判断(Hook 是确定性脚本,不调用 LLM)

7.3 调试 Hook

Hook 出问题时的排查方式:

bash
# 手动测试 Hook,模拟 stdin 输入
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | .claude/hooks/pre-bash-firewall.sh
# 应输出包含 permissionDecision: "deny" 的 JSON

echo '{"tool_name":"Bash","tool_input":{"command":"npm test"}}' | .claude/hooks/pre-bash-firewall.sh
echo $?  # 应该输出 0(放行)

7. 失败恢复

四份参考文件都没有覆盖的盲区——Hook 失败时怎么办:

场景处理策略
格式化 Hook 失败(prettier 未安装)`
安全 Hook 误判(拦截了正常命令)修改 Hook 的正则模式,放宽匹配
Hook 执行超时检查 Hook 脚本,优化或移除耗时操作
所有 Hook 都不工作检查 settings.json 的 hooks 配置格式和文件路径

8. 与其他子系统的关系

CLAUDE.md:减轻负担
  "不需要写格式化规则" → Hook 自动处理

Skills:互补
  Skill 负责流程编排 → Hook 负责自动质量关卡

SubAgents:同样生效
  子代理的工具调用同样受 PreToolUse Hook 拦截

settings.json permissions:协同
  permissions 控制"允许/禁止" → Hook 控制"自动处理"

9. Hook 类型扩展

除标准 Shell Command Hooks 外,Claude Code 还支持以下高级 Hook 类型。

9.1 HTTP Hooks

通过 type: http 将事件 POST 到外部端点,替代本地脚本。支持自定义 headers 和环境变量插值:

jsonc
// settings.json
{
  "hooks": {
    "NotificationSend": [{
      "type": "http",
      "url": "https://your-server.com/webhook",
      "headers": {
        "Authorization": "Bearer ${API_TOKEN}",  // env var interpolation
        "X-Project": "my-project"
      },
      "transport": "sse"  // 支持 sse | fetch
    }]
  }
}
  • headers 中的 ${VAR_NAME} 会自动替换为同名环境变量
  • 适用场景:集中日志收集、团队协作通知、远程审计、CI/CD 集成

9.2 Prompt Hooks

Prompt 事件在 API 请求发送前触发,允许修改发送给模型的完整 prompt:

  • 注入额外上下文(如从外部知识库检索的 RAG 片段)
  • 审计/记录所有发送的 prompt 内容
  • 对敏感信息进行脱敏处理
jsonc
{
  "hooks": {
    "Prompt": [{
      "type": "command",
      "command": "python3 scripts/prompt-filter.py"
    }]
  }
}

9.3 SubagentStart 事件

子代理启动前触发,可用于:

  • 为特定类型的子代理注入定制指令
  • 基于策略阻止不必要的子代理启动
  • 记录子代理的启动参数用于调试

9.4 Skill 范围内的 Hooks

Skills 和 Agents 可通过 frontmatter 的 hooks 字段声明专属 Hook,仅在该 Skill 激活期间生效,Skill 结束后自动移除:

yaml
---
name: review
hooks:
  PostToolUse:
    - command: ".claude/hooks/strict-lint-check.sh"
      event_filter: "Write|Edit"
  PreToolUse:
    - command: ".claude/hooks/block-force-push.sh"
      event_filter: Bash
---
# 以下是 Skill 正文...

典型用例:

  • /review Skill 激活时启用更严格的 Lint 检查
  • /deploy Skill 激活时额外验证部署环境变量
  • 避免全局 Hook 配置过重,按需加载

面向个人开发者的 AI 辅助编程工程化方案