第 42 章

Skills 开发:SKILL.md 全参数 / 动态上下文注入 / 调用控制矩阵

第四十二章:Claude Code Hooks:pre-tool、post-tool 与自动化质量门控

42.1 Hooks 的设计动机

Claude Code 是一个强大的 AI 编程助手,但在团队和生产环境中使用时,你需要一种机制来确保 Claude 的每个操作都符合规范——不只是通过 CLAUDE.md 告知 Claude 规范是什么,而是在行为发生的前后自动强制执行这些规范。

这就是 Claude Code Hooks 的存在价值。

Hooks 是在 Claude 执行工具调用前后触发的 Shell 命令钩子。它们在 .claude/settings.json 中配置,完全独立于 Claude 的 AI 决策之外运行。无论 Claude 决定做什么,Hooks 都会在定义的节点执行你指定的命令。

核心价值:Hooks 把团队规范从"建议"变成了"强制执行"。

考虑这样一个场景:你的团队规定所有 Python 文件修改后必须运行 Black 格式化。有了 post-tool Hooks,每当 Claude 写入或修改 .py 文件后,Black 就会自动运行——不需要提醒 Claude,不需要依赖 Claude 的主动性,格式化就自动发生了。

42.2 Hooks 的配置格式

Hooks 在 .claude/settings.json 中配置。这个文件是 Claude Code 的项目级配置文件,应该提交到版本控制:

{
  "hooks": {
    "pre-tool": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'About to modify files...' >> .claude/hooks.log"
          }
        ]
      }
    ],
    "post-tool": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "scripts/post-write-hook.sh"
          }
        ]
      }
    ]
  }
}

配置结构解析

hooks
├── pre-tool              # 工具执行前触发的 Hooks 列表
│   └── [Hook Entry]
│       ├── matcher       # 匹配哪些工具(正则表达式)
│       └── hooks         # 要执行的命令列表
│           └── [Hook]
│               ├── type  # 目前只支持 "command"
│               └── command  # 要执行的 Shell 命令
└── post-tool             # 工具执行后触发的 Hooks 列表
    └── [同结构]

matcher 的写法

matcher 是一个正则表达式字符串,用于匹配工具名称。Claude Code 的主要工具名称如下:

文件操作类:
- Write          # 写入新文件
- Edit           # 编辑现有文件(单处替换)
- MultiEdit      # 多处编辑
- Read           # 读取文件

Shell 操作类:
- Bash           # 执行 Shell 命令

搜索类:
- Grep           # 文本搜索
- Glob           # 文件名搜索

其他:
- WebFetch       # 获取网页内容
- TodoWrite      # 写入 Todo 列表

Matcher 示例:

"matcher": "Write|Edit|MultiEdit"   // 匹配所有写操作
"matcher": "Bash"                   // 只匹配 Bash 工具
"matcher": ".*"                     // 匹配所有工具
"matcher": "Write"                  // 只匹配 Write

42.3 pre-tool Hooks:在操作前执行

pre-tool Hooks 在 Claude 执行工具之前触发。它们的主要用途是:

示例一:防止在主分支直接提交

{
  "hooks": {
    "pre-tool": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "scripts/check-branch.sh"
          }
        ]
      }
    ]
  }
}

scripts/check-branch.sh

#!/bin/bash
# 检查当前 git 分支,如果是 main 或 master,输出警告

BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
    echo "WARNING: You are on the $BRANCH branch. Direct commits to $BRANCH are discouraged."
    echo "Consider creating a feature branch first: git checkout -b feature/your-feature"
    # 返回非零退出码会阻止工具执行(Claude Code 会中止并向用户报告)
    # 这里我们只是警告,不阻止:
    exit 0
fi
exit 0

示例二:记录所有文件操作审计日志

{
  "hooks": {
    "pre-tool": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"$(date -Iseconds) PRE-TOOL: $CLAUDE_TOOL_NAME $CLAUDE_TOOL_INPUT\" >> .claude/audit.log"
          }
        ]
      }
    ]
  }
}

关于 pre-tool 的退出码

pre-tool Hook 的退出码有重要含义:

这让 pre-tool Hooks 成为真正的"质量门控"——你可以编写检查脚本,在不满足条件时直接阻止 Claude 的操作。

示例三:阻止修改生产配置文件

#!/bin/bash
# pre-write-check.sh
# 阻止 Claude 修改特定的生产配置文件

PROTECTED_FILES=(
    "config/production.yaml"
    "config/production.json"
    ".env.production"
    "kubernetes/prod/"
)

# 从环境变量读取 Claude 即将操作的文件路径
TARGET_FILE="${CLAUDE_TOOL_INPUT_FILE_PATH:-}"

for protected in "${PROTECTED_FILES[@]}"; do
    if [[ "$TARGET_FILE" == *"$protected"* ]]; then
        echo "ERROR: Modification of protected production config file is not allowed."
        echo "File: $TARGET_FILE"
        echo "To modify production configs, use the deployment pipeline."
        exit 1  # 非零退出码,阻止操作
    fi
done

exit 0

42.4 post-tool Hooks:在操作后执行

post-tool Hooks 在 Claude 的工具执行完成后触发。它们的主要用途是:

示例一:Python 文件自动格式化

{
  "hooks": {
    "post-tool": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "scripts/post-write-format.sh"
          }
        ]
      }
    ]
  }
}

scripts/post-write-format.sh

#!/bin/bash
# 对 Python 文件执行自动格式化和 lint

# 获取被修改的文件(从环境变量)
MODIFIED_FILE="${CLAUDE_TOOL_INPUT_FILE_PATH:-}"

if [[ "$MODIFIED_FILE" == *.py ]]; then
    echo "Running Black formatter on $MODIFIED_FILE..."
    black "$MODIFIED_FILE" 2>&1
    
    echo "Running isort on $MODIFIED_FILE..."
    isort "$MODIFIED_FILE" 2>&1
    
    echo "Running flake8 lint check..."
    flake8 "$MODIFIED_FILE" 2>&1
    LINT_EXIT=$?
    
    if [ $LINT_EXIT -ne 0 ]; then
        echo "WARNING: flake8 found issues in $MODIFIED_FILE"
        # post-tool 的非零退出码通常不会阻止 Claude,但会在输出中显示
    fi
fi

exit 0

示例二:TypeScript 文件类型检查

{
  "hooks": {
    "post-tool": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "scripts/ts-typecheck.sh"
          }
        ]
      }
    ]
  }
}

scripts/ts-typecheck.sh

#!/bin/bash
# 对修改的 TypeScript 文件执行类型检查

MODIFIED_FILE="${CLAUDE_TOOL_INPUT_FILE_PATH:-}"

if [[ "$MODIFIED_FILE" == *.ts ]] || [[ "$MODIFIED_FILE" == *.tsx ]]; then
    echo "Running TypeScript type check..."
    # 使用 tsc --noEmit 检查类型,不生成输出文件
    npx tsc --noEmit --project tsconfig.json 2>&1
    TS_EXIT=$?
    
    if [ $TS_EXIT -ne 0 ]; then
        echo "TypeScript type errors found. Claude should review and fix."
        exit 1  # 通知 Claude 有问题需要处理
    fi
fi

exit 0

当 post-tool Hook 返回非零退出码时,Claude Code 会将错误信息反馈给 Claude,Claude 可以据此继续修复问题。这形成了一个自动反馈循环:Claude 修改代码 → Hook 运行类型检查 → 发现错误 → 反馈给 Claude → Claude 继续修复。

42.5 环境变量:Hook 如何获取上下文

Hooks 通过环境变量获取关于当前操作的上下文信息:

# Claude Code 注入到 Hook 进程的环境变量

CLAUDE_TOOL_NAME          # 触发 Hook 的工具名称(如 "Write")
CLAUDE_TOOL_INPUT         # 工具的完整输入(JSON 格式)
CLAUDE_TOOL_INPUT_FILE_PATH  # 对于文件操作,目标文件路径
CLAUDE_TOOL_INPUT_COMMAND    # 对于 Bash 工具,要执行的命令
CLAUDE_TOOL_OUTPUT        # (仅 post-tool)工具的输出结果
CLAUDE_SESSION_ID         # 当前会话 ID
CLAUDE_PROJECT_DIR        # 项目根目录路径

解析 JSON 输入

对于需要更详细信息的 Hook,可以解析 CLAUDE_TOOL_INPUT JSON:

#!/bin/bash
# 解析工具输入以获取详细信息

TOOL_INPUT="${CLAUDE_TOOL_INPUT:-{}}"

# 使用 jq 解析 JSON
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty')
CONTENT=$(echo "$TOOL_INPUT" | jq -r '.content // empty')
COMMAND=$(echo "$TOOL_INPUT" | jq -r '.command // empty')

echo "Tool: $CLAUDE_TOOL_NAME"
echo "File: $FILE_PATH"
echo "Command: $COMMAND"

42.6 构建自动化质量门控系统

通过组合 pre-tool 和 post-tool Hooks,可以构建一套完整的自动化质量门控系统。以下是一个面向 Node.js 项目的完整配置示例:

{
  "hooks": {
    "pre-tool": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "scripts/hooks/pre-bash.sh"
          }
        ]
      },
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "scripts/hooks/pre-write.sh"
          }
        ]
      }
    ],
    "post-tool": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "scripts/hooks/post-write.sh"
          }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "scripts/hooks/post-bash.sh"
          }
        ]
      }
    ]
  }
}

scripts/hooks/post-write.sh(核心质量门控):

#!/bin/bash
set -e

MODIFIED_FILE="${CLAUDE_TOOL_INPUT_FILE_PATH:-}"
EXIT_CODE=0

# 1. JavaScript/TypeScript 文件处理
if [[ "$MODIFIED_FILE" =~ \.(js|jsx|ts|tsx)$ ]]; then
    
    # 自动格式化(不是 lint,Prettier 会直接修改文件)
    if command -v prettier &>/dev/null; then
        prettier --write "$MODIFIED_FILE" 2>&1
        echo "✓ Prettier formatting applied"
    fi
    
    # ESLint 检查(返回 lint 结果,不自动修复)
    if command -v eslint &>/dev/null; then
        eslint "$MODIFIED_FILE" 2>&1
        ESLINT_EXIT=$?
        if [ $ESLINT_EXIT -ne 0 ]; then
            echo "✗ ESLint found issues"
            EXIT_CODE=$ESLINT_EXIT
        else
            echo "✓ ESLint passed"
        fi
    fi
fi

# 2. 测试文件检测(如果修改了业务文件,检查对应测试是否存在)
if [[ "$MODIFIED_FILE" =~ src/(.+)\.(ts|js)$ ]] && [[ ! "$MODIFIED_FILE" =~ \.(test|spec)\. ]]; then
    RELATIVE_PATH="${BASH_REMATCH[1]}"
    TEST_FILE="src/${RELATIVE_PATH}.test.ts"
    SPEC_FILE="src/${RELATIVE_PATH}.spec.ts"
    
    if [ ! -f "$TEST_FILE" ] && [ ! -f "$SPEC_FILE" ]; then
        echo "WARNING: No test file found for $MODIFIED_FILE"
        echo "Consider creating: $TEST_FILE"
    fi
fi

# 3. package.json 变更检测
if [[ "$MODIFIED_FILE" == "package.json" ]]; then
    echo "package.json modified. Running npm install to sync node_modules..."
    npm install 2>&1
fi

exit $EXIT_CODE

42.7 Hooks 与 CLAUDE.md 的协同

Hooks 和 CLAUDE.md 是互补的工具,各自解决不同层面的问题:

维度 CLAUDE.md Hooks
执行方式 影响 Claude 的 AI 决策 独立于 AI,Shell 命令执行
可靠性 依赖 Claude 遵守 机械强制执行
适用场景 风格指南、架构决策 格式化、lint、测试
可绕过性 Claude 可能忘记或忽略 无法被忽略
灵活性 高(自然语言描述) 低(必须是可执行脚本)

最佳实践是两者结合

例如,CLAUDE.md 说"所有 TypeScript 文件必须通过类型检查",同时配置 post-tool Hook 在每次文件写入后自动运行 tsc --noEmit。这样即使 Claude 忘记了规范,Hook 也会捕获问题并反馈。

42.8 调试 Hooks

Hooks 出问题时,调试可能会比较困难。以下是一些实用技巧:

启用 Hook 日志

# 在 Hook 脚本开头添加日志
LOG_FILE=".claude/hooks-debug.log"
echo "$(date -Iseconds) HOOK: $CLAUDE_TOOL_NAME" >> "$LOG_FILE"
echo "  INPUT: $CLAUDE_TOOL_INPUT" >> "$LOG_FILE"

在 CLAUDE.md 中说明调试方法

## Hook 调试

如果 Hooks 出现问题,检查 .claude/hooks-debug.log 文件。
运行 `scripts/hooks/test-hooks.sh` 可以在不涉及 Claude 的情况下测试所有 Hook。

本地测试 Hook 脚本

# 模拟 Claude Code 的环境变量,直接测试 Hook
CLAUDE_TOOL_NAME="Write" \
CLAUDE_TOOL_INPUT_FILE_PATH="src/utils/format.ts" \
CLAUDE_TOOL_INPUT='{"file_path":"src/utils/format.ts","content":"export function foo() {}"}' \
bash scripts/hooks/post-write.sh

小结

Claude Code Hooks 是将 AI 辅助编程从"随机质量"提升为"可预测质量"的关键机制。

关键要点:

本章评分
4.7  / 5  (3 评分)

💬 留言讨论