Hooksで自動コードチェック:書いた瞬間に品質保証

PostToolUse HookでClaudeがファイルを書いた後に自動でlintを実行。終了コード2でエラーをClaudeに返し、書く→チェック→修正のループを自動化。


Claude Codeはコードを速く書きますが、lintを実行することを忘れることがあります。「書いたついでにチェックして」と頼んでも、やってくれる時もあれば漏れる時もある。Hooksはこの問題を解決します——コードチェックを「Claudeの責任」から「システムの責任」へ移すのです。

この記事では一点に絞って解説します:PostToolUse HookでClaudeがコードを書いた後に自動チェックをトリガーし、すべてのファイル変更がコーディング規約を満たす状態にする方法です。


アプローチ:PostToolUse + ファイルタイプフィルタリング

コードチェックHookのロジックはシンプルです:

  1. ClaudeがWriteまたはEditでファイルを書く
  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  # 元の終了コードではなく2を使い、Claudeが確実に対処するようにする
  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'"
          }
        ]
      }
    ]
  }
}

head -20で出力を制限——tscのエラーは長くなりがちで、大量のテキストをClaudeに渡すと判断を妨げます。


チェック不要なファイルを除外

#!/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. 2回目のチェック通過——完了

手動でチェックコマンドを実行する必要は一切ありません。


グローバル vs プロジェクト単位

グローバル(~/.claude/settings.json): すべてのプロジェクトで同じ規約を使う場合に適切。

プロジェクト単位(.claude/settings.jsonをgitにコミット): チームプロジェクトに推奨。リポジトリを開くだけで正しい設定が自動的に適用されます。

両者は共存可能で、プロジェクト単位のhooksはグローバルとマージして両方実行されます。


まとめ

Hooksで自動コードチェックを実現する核心は3ステップです:

  1. PostToolUse + "Write|Edit" matcherでファイル書き込みを捕捉
  2. ファイルパスを取得し、拡張子で対応するlintツールに振り分ける
  3. 失敗時は終了コード2でエラーをClaudeにフィードバックし、自動修正させる

1つの言語から始めて、動いたら他の言語を追加する。スクリプトを.claude/hooks/にコミットしてチームで共有すれば、一度の設定で長期的に効果が続きます。