用 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

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 自動修復的關鍵:

#!/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
  fi

  rubocop -A "$file" --no-color -q
}

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

型別檢查:獨立 Hook

型別檢查通常比 lint 慢,適合單獨設定:

{
  "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'"
          }
        ]
      }
    ]
  }
}

排除不需要檢查的檔案

#!/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): 適合所有專案使用同一套規範。

專案級(.claude/settings.json 提交到 git): 團隊專案推薦,每個人開啟專案就自動有正確的設定。

兩者可以並存,專案級會和全域合併執行。


小結

用 Hooks 做自動程式碼檢查的核心就三步:

  1. PostToolUse + "Write|Edit" matcher 攔截檔案寫入
  2. 從環境變數取得檔案路徑,按副檔名分發給對應的 lint 工具
  3. 失敗時用退出碼 2 把錯誤回饋給 Claude,讓它自己修

從一個語言開始配,跑通了再加其他語言。把腳本提交到 .claude/hooks/,團隊共用,一次設定長期受益。