用 Hooks 做自动代码检查:写完即达标

用 PostToolUse Hook 在 Claude 写文件后自动触发 lint,配合退出码 2 让 Claude 自动修复,形成写→查→修闭环。


Claude Code 写代码很快,但它不总是记得跑 lint。你让它"写完顺便检查一下",它有时会做,有时会漏。Hooks 解决的就是这个问题——把代码检查从"Claude 的责任"变成"系统的责任"。

这篇文章专注一件事:用 PostToolUse Hook 在 Claude 写完代码后自动触发检查,让每次文件变更都符合你的代码规范。


思路:PostToolUse + 文件类型过滤

代码检查的 Hook 逻辑很简单:

  1. Claude 调用 WriteEdit 写了一个文件
  2. Hook 拦截这个事件,拿到文件路径
  3. 根据文件后缀,决定跑哪个检查工具
  4. 检查通过:退出码 0,Claude 继续
  5. 检查失败:退出码 2,把错误输出反馈给 Claude,让它修

关键是退出码 2——它会让 Claude 收到错误信息并自动修复,形成一个"写→查→修"的闭环。


基础配置:单语言项目

Ruby 项目(RuboCop)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'file=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .file_path 2>/dev/null); [[ \"$file\" == *.rb ]] && rubocop -A \"$file\" --no-color -q || true'"
          }
        ]
      }
    ]
  }
}

-A 参数让 RuboCop 自动修复可修复的问题。不可修复的问题会输出到 stderr,退出码非零,Claude 收到后会处理。

JavaScript/TypeScript 项目(ESLint)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'file=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .file_path 2>/dev/null); [[ \"$file\" =~ \\.(js|ts|jsx|tsx)$ ]] && npx eslint --fix \"$file\" || true'"
          }
        ]
      }
    ]
  }
}

Python 项目(ruff)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'file=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .file_path 2>/dev/null); [[ \"$file\" == *.py ]] && ruff check --fix \"$file\" && ruff format \"$file\" || true'"
          }
        ]
      }
    ]
  }
}

进阶配置:多语言项目

实际项目往往混用多种语言。把检查逻辑写进独立脚本,比在 JSON 里堆命令更好维护。

创建 .claude/hooks/lint.sh

#!/bin/bash
set -e

# 从 stdin 读取工具调用信息
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')

# 没有文件路径,退出
[[ -z "$file" ]] && exit 0

# 文件不存在(可能是删除操作),退出
[[ ! -f "$file" ]] && exit 0

ext="${file##*.}"

case "$ext" in
  rb)
    rubocop -A "$file" --no-color -q
    ;;
  js|ts|jsx|tsx)
    npx eslint --fix "$file" --quiet
    ;;
  py)
    ruff check --fix "$file"
    ruff format "$file"
    ;;
  go)
    gofmt -w "$file"
    golangci-lint run "$file" 2>&1
    ;;
  *)
    # 不认识的类型,跳过
    exit 0
    ;;
esac

settings.json 引用这个脚本:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/lint.sh"
          }
        ]
      }
    ]
  }
}

好处:脚本可以加注释、可以单独测试、可以提交到 git 和团队共享。


让 Claude 自动修复

退出码 2 + stderr 输出是让 Claude 自动修复的关键。默认情况下,lint 工具失败时会以非零退出码退出,Claude 会收到错误但可能不会立即重试。

显式用退出码 2 来强制反馈:

#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')

[[ -z "$file" || ! -f "$file" ]] && exit 0

ext="${file##*.}"

run_lint() {
  local output exit_code
  output=$(rubocop "$file" --no-color 2>&1)
  exit_code=$?

  if [[ $exit_code -ne 0 ]]; then
    echo "$output" >&2
    exit 2  # 用 2 而非原始退出码,确保 Claude 收到并处理
  fi

  # 先自动修复,再检查
  rubocop -A "$file" --no-color -q
}

[[ "$ext" == "rb" ]] && run_lint

这样 Claude 会看到具体的错误信息,并在下一轮自动修复。


类型检查:独立 Hook

类型检查通常比 lint 慢,适合单独配置,避免每次小改动都等待全量类型检查。

TypeScript 类型检查(仅对 .ts 文件):

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'file=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .file_path 2>/dev/null); [[ \"$file\" == *.ts ]] && npx tsc --noEmit --skipLibCheck 2>&1 | head -20 || true'"
          }
        ]
      }
    ]
  }
}

head -20 截断输出——tsc 报错有时候很长,喂给 Claude 太多反而干扰它的判断。


排除不需要检查的文件

有些文件不应该被 lint:自动生成的文件、vendor 目录、测试 fixture 等。

#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')

[[ -z "$file" || ! -f "$file" ]] && exit 0

# 排除规则
skip_patterns=(
  "vendor/"
  "node_modules/"
  "db/schema.rb"
  "db/queue_schema.rb"
  ".min.js"
  "_test.go"
)

for pattern in "${skip_patterns[@]}"; do
  [[ "$file" == *"$pattern"* ]] && exit 0
done

# 正常执行检查
ext="${file##*.}"
[[ "$ext" == "rb" ]] && rubocop -A "$file" --no-color -q

实际效果

配置好之后的工作流变成这样:

  1. 你让 Claude 写一个 Ruby 文件
  2. Claude 用 Write 工具写入
  3. Hook 自动跑 rubocop -A,发现 3 处风格问题
  4. RuboCop 自动修复其中 2 处,第 3 处需要人工判断
  5. 第 3 处错误通过 stderr 反馈给 Claude
  6. Claude 修改代码,Hook 再次触发
  7. 第二轮检查通过,继续

全程你不需要手动跑任何检查命令。


全局 vs 项目级的选择

放全局(~/.claude/settings.json): 如果你所有项目都用同一套规范,比如个人项目统一用 ESLint + RuboCop。

放项目级(.claude/settings.json 提交到 git): 团队项目推荐这样做,每个人打开项目就自动有正确的检查配置,不需要额外设置。

两者可以并存,项目级会和全局合并执行。


小结

用 Hooks 做自动代码检查的核心就三步:

  1. PostToolUse + "Write|Edit" matcher 拦截文件写入
  2. 从环境变量拿文件路径,按后缀分发给对应的 lint 工具
  3. 失败时用退出码 2 把错误反馈给 Claude,让它自己修

从一个语言开始配,跑通了再加其他语言。把脚本提交到 .claude/hooks/,团队共享,一次配置长期受益。