用 PostToolUse Hook 在 Claude 写文件后自动触发 lint,配合退出码 2 让 Claude 自动修复,形成写→查→修闭环。
Claude Code 写代码很快,但它不总是记得跑 lint。你让它"写完顺便检查一下",它有时会做,有时会漏。Hooks 解决的就是这个问题——把代码检查从"Claude 的责任"变成"系统的责任"。
这篇文章专注一件事:用 PostToolUse Hook 在 Claude 写完代码后自动触发检查,让每次文件变更都符合你的代码规范。
代码检查的 Hook 逻辑很简单:
Write 或 Edit 写了一个文件关键是退出码 2——它会让 Claude 收到错误信息并自动修复,形成一个"写→查→修"的闭环。
{
"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 收到后会处理。
{
"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'"
}
]
}
]
}
}
{
"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 和团队共享。
退出码 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 会看到具体的错误信息,并在下一轮自动修复。
类型检查通常比 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
配置好之后的工作流变成这样:
Write 工具写入rubocop -A,发现 3 处风格问题全程你不需要手动跑任何检查命令。
放全局(~/.claude/settings.json): 如果你所有项目都用同一套规范,比如个人项目统一用 ESLint + RuboCop。
放项目级(.claude/settings.json 提交到 git): 团队项目推荐这样做,每个人打开项目就自动有正确的检查配置,不需要额外设置。
两者可以并存,项目级会和全局合并执行。
用 Hooks 做自动代码检查的核心就三步:
PostToolUse + "Write|Edit" matcher 拦截文件写入从一个语言开始配,跑通了再加其他语言。把脚本提交到 .claude/hooks/,团队共享,一次配置长期受益。