Claude Media
Claude Codeでhuskyとpre-commitを併用し、失敗したコミットを直させる

Claude Codeでhuskyとpre-commitを併用し、失敗したコミットを直させる

huskyとpre-commitのGitフックでコミットが落ちたとき、Claude Codeに出力を読ませて直させる流れと、--no-verifyをCLAUDE.mdとPreToolUse hookで封じる設定をまとめます。

Gitフックの失敗はClaudeに直させ、迂回は仕組みで止める

huskyやpre-commitを入れたリポジトリでは、Claude Codeがgit commitを実行するたびにGitフックが走ります。Lintやテストが落ちればコミットは通りません。ここでClaudeに直させる運用は、2つの部品でできています。

  1. 失敗した出力を読んで原因を直し、コミットをやり直す(CLAUDE.mdで指示する)
  2. フックを飛ばして通す近道を、PreToolUse hookで物理的に塞ぐ

1つ目は「お願い」で、2つ目は「強制」です。CLAUDE.mdはコンテキストであって強制設定ではない、とメモリの解説に明記されています。ブロックしたい操作はPreToolUse hookを使う、という切り分けです。

Claude側のhookでLintを先回りして直す設計は、PostToolUse hookのLint自動修正が扱っています。ここでは、それでも取りこぼした指摘がGitフックで落ちたあとの流れを組みます。

huskyとpre-commitは何が違うのか

どちらもGitフックを管理する道具ですが、持っている責務が違います。

項目huskypre-commit
フックの置き場husky.husky/pre-commitなどのスクリプトpre-commit.pre-commit-config.yamlに宣言
中身の書き方huskyシェルスクリプトを直接書くpre-commit公開リポジトリのhookをidで指定
導入コマンドhuskynpx husky initpre-commitpre-commit install
主な想定huskyNode.jsプロジェクトのnpmスクリプトpre-commit言語を問わない検査の集合

huskyはhusky initで.husky/にpre-commitスクリプトを作り、package.jsonのprepareスクリプトを更新します。フックの追加はファイルを1つ作るだけで、例としてecho "npm test" > .husky/pre-commitです。

pre-commitは.pre-commit-config.yamlにreposとhooksを書き、pre-commit installでGitフックを設置します。最小構成は次の形です。

repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v2.3.0
    hooks:
    -   id: check-yaml
    -   id: end-of-file-fixer
    -   id: trailing-whitespace

併用するときはGitフックの入口を1本にする

両方を入れると、Gitフックの入口が2つになります。Claudeに読ませる失敗出力が二重に出たり、どちらが落としたのか分かりにくくなったりします。入口はhuskyの.husky/pre-commitに1本化し、その中からpre-commitを呼ぶ構成が整理しやすい形です(この構成は筆者の設計案で、併用手順として文書化されたものではありません)。

# .husky/pre-commit
pre-commit run

pre-commit runは、ステージ済みの変更に対して設定済みのhookを実行します。リポジトリ全体に走らせたいときはpre-commit run --all-filesです。新しいhookを足したときは、まず全ファイルへ走らせておくのが通例です。

既存のGitフックがある環境でpre-commit installすると、既存のフックとpre-commitのフックを両方走らせる移行モードで入る、とpre-commitのドキュメントにあります。上書きしたい場合は-f(--overwrite)を渡します。

Claudeには何が見えて、何を直すのか

コミットがフックで落ちると、Claudeはgit commitの出力からどのhookが何と言って失敗したかを読めます。pre-commitの出力には、hookごとにPassed/Failedとhook idが並びます。ドキュメントの例では次のように出ます。

Trim Trailing Whitespace.................................................Failed
- hook id: trailing-whitespace
- exit code: 1
 
Files were modified by this hook. Additional output:
 
Fixing sample.py

失敗の型で、Claudeがやるべきことは変わります。

失敗の型出力の手がかりClaudeの動き
Lint・型・テストの指摘出力の手がかり該当ファイルと行、非ゼロのexit codeClaudeの動き指摘箇所を直し、再ステージして再コミット
自動修正hookがファイルを書き換えた出力の手がかりFiles were modified by this hookClaudeの動き変更を確認してステージし直し、再コミット
環境の問題出力の手がかりcommand not foundなどClaudeの動き直そうとせず、原因を人に報告

2行目が見落とされやすい型です。pre-commitのフォーマッタ系hookは、ファイルを書き換えた時点で「失敗」として返り、コミットは止まります。hookの要件も「失敗時は非ゼロで終了するか、ファイルを変更すること」です。直ったのに落ちた、と見えるので、Claudeが--no-verifyで通そうとする引き金になり得ます。修正内容をgit diffで確かめてgit addし直すだけで次は通る、と指示しておくと安定します。

3行目の環境問題も切り分けが要ります。huskyの公式How Toは、nvmやfnmなどのバージョンマネージャー経由でNodeを入れている場合、GUIアプリから走るGitフックでPATHが通らずcommand not foundになり得ると説明しています。これはコードの問題ではないので、Claudeに何度も直させても進みません。

対処は人の側で行います。huskyはフックの実行前に、ホームの設定ファイルからローカルコマンドを読み込みます。読み込み元は$XDG_CONFIG_HOME/husky/init.shと~/.config/husky/init.shで、~/.huskyrcは非推奨です。WindowsではC:\Users\ユーザー名\.config\husky\init.shを使います。nvmなら、このinit.shでnvmを読み込ませる形が考えられます。

# ~/.config/husky/init.sh(nvmの例)
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"

この書き込み例はnvmの標準的な読み込み方に合わせた筆者の例で、huskyの文書に載っているのはファイルの場所と、そこから読み込まれるという仕様までです。置き場所がホームディレクトリ配下なので、この設定はリポジトリには入らず、コミットが落ちるマシンごとに必要です。設定後は、ターミナルとGUIの両方からコミットして確かめます。

CLAUDE.mdに書く指示

失敗時の動きと禁止事項を、CLAUDE.mdに短く書きます。長文にせず、判断基準だけを置きます。

## コミットとGitフック
 
- コミットがGitフック(husky / pre-commit)で失敗したら、出力を最後まで読み、原因のコードを直して再コミットする
- フォーマッタがファイルを書き換えて失敗した場合は、`git diff`で変更を確認し、`git add`してから再コミットする
- `--no-verify`、`-n`、`HUSKY=0`、`SKIP=`でフックを飛ばさない。通らないときは原因を報告する
- 同じ原因で2回続けて失敗したら、それ以上試さずに状況を説明する

ここで挙げた迂回手段は、どれも正規の機能です。huskyのHow Toには、Gitの-n/--no-verifyで1コマンドだけフックを飛ばせること、--no-verifyのないコマンドにはHUSKY=0を前置できることが書かれています。pre-commitには、hook idをカンマ区切りで並べるSKIP環境変数があり、ドキュメントは「コミット全体を--no-verifyするより1つのhookだけ飛ばせる」利点として紹介しています。人間には便利な逃げ道が、Claudeにとっては「テストを通す近道」にもなります。

PreToolUse hookで迂回を止める

CLAUDE.mdだけでは、Claudeが指示より目の前のエラー解消を優先する余地が残ります。公式は、PreToolUse hookならツール呼び出しを実行前に止められ、permissionDecision: "deny"はbypassPermissionsモードや--dangerously-skip-permissionsでもブロックすると説明しています。

止め方はexit code 2です。Bashの場合、stdinのJSONにtool_input.commandが入ります。exit 2のときにstderrへ書いた理由は、Claudeへのフィードバックとして返り、Claudeは別のやり方を探せます。

#!/bin/bash
# .claude/hooks/block-hook-bypass.sh
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
 
# git commit / git push とその前置き環境変数を対象にする
if ! echo "$CMD" | grep -Eq '(^|[[:space:];&|])git[[:space:]]+(commit|push)'; then
  exit 0
fi
 
if echo "$CMD" | grep -Eq '(--no-verify|[[:space:]]-[a-zA-Z]*n[a-zA-Z]*([[:space:]]|$)|HUSKY=0|SKIP=)'; then
  echo "Blocked: Gitフックを飛ばす操作は禁止です。フックの出力を読んで原因を直してください。" >&2
  exit 2
fi
 
exit 0

-nはgit commitではフックを飛ばす短縮形ですが、-nmのようにほかの短いフラグと束ねられることもあるため、正規表現で束ねた形も拾っています。反面、-nを別の意味で使うオプションを巻き込む可能性があります。たとえばgit push -nは--dry-runの意味で、この正規表現は止めてしまいます。git commitとgit pushだけに絞る前段の判定は、その誤検知を減らすためのものです。

登録は.claude/settings.jsonで、Bashだけにマッチさせます。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-hook-bypass.sh"
          }
        ]
      }
    ]
  }
}

スクリプトには実行権限が必要です。付け忘れると動きません。

chmod +x .claude/hooks/block-hook-bypass.sh

hookのifフィールド("if": "Bash(git *)"など)でhookの起動を絞ることもできます。ただしこの絞り込みはベストエフォートで、確実な許可・拒否にはhookでなく権限システムを使うよう、公式ガイドは案内しています。ifは前置した環境変数まで確実に照合するとは限らないため、スクリプト側でコマンド全体を検査します。

権限のdenyルールを併用する

権限側でも、拒否ルールを足せます。denyはallowより優先され、*はルールのどの位置にも置けます。

{
  "permissions": {
    "deny": [
      "Bash(git commit *--no-verify*)",
      "Bash(git push *--no-verify*)"
    ]
  }
}

環境変数の前置や-nの束ね書きは、この形では拾えない可能性があります。hookのスクリプトと役割を分け、denyは明白な--no-verifyの保険と考えるのが無難です。

動作確認のしかた

設定が効いているかは、Claudeに実際に迂回を頼んで確かめます。

  1. テストが必ず落ちる小さな変更を作り、ステージする
  2. 「フックが通らないのでgit commit --no-verifyでコミットして」と頼む
  3. Blocked:のメッセージがClaudeに返り、フックの出力を読んで直そうとする動きになるかを見る

hookが発火したかは、Ctrl+Oのトランスクリプトで確認できます。詳細はclaude --debug-file /tmp/claude.logで取れ、実行中なら/debugでも有効にできます。JSON出力を使う場合、permissionDecisionをhookSpecificOutputの外に置くと無視されるので注意してください。この記事のスクリプトはexit 2を使うため、その落とし穴は避けられます。

つまずきやすい点

  • フォーマッタが毎回落とす: 自動修正hookはファイルを書き換えたコミットを一度止めます。Claude側でPostToolUseのフォーマッタを先に走らせておけば、この往復が減ります。組み方はPostToolUse hookのLint自動修正と、テスト自動実行のPostToolUseとStopで扱っています。
  • モノレポで全部が走る: 変更のあったパッケージだけを検査したいなら、NxのaffectedをClaude Codeのhookから使う設計が参考になります。pre-commitは設定ファイル側でも対象を絞れます。全体にはfilesとexcludeの正規表現があり、hookごとにはpass_filenames(既定はtrue、falseならファイル名を渡さない)とstages(どのGitフックで走らせるか)を指定できます。
default_stages: [pre-commit]   # hookごとにstagesを書かなければこれが効く
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v2.3.0
    hooks:
    -   id: check-yaml
        files: ^services/api/   # このパスの変更だけを検査(例)

上のfilesのパスは例で、default_stagesの指定はドキュメントにある形です。default_stagesはstagesを書いていないhookにだけ効き、hook側で個別に指定したstagesはそちらが優先されます。

  • 初回のpre-commitが遅い: 初めて走るhookは環境を用意するため時間がかかります。pre-commitのドキュメントによると、node未導入のマシンではnodeをダウンロードしてビルドします。Claudeがタイムアウトと勘違いして迂回に走らないよう、最初は人間がpre-commit run --all-filesで暖めておくと安全です。
  • fail_fastの設定: pre-commitはfail_fast: trueにすると最初の失敗で止まります。既定はfalseで、全部のhookの結果が一度に出ます。Claudeに直させる用途では、指摘がまとまって出る既定のほうが往復が少なく済みます。

まとめ

Gitフックが落ちたコミットをClaudeに直させる設計は、指示と強制の二層です。失敗の型ごとの動き(指摘を直す・書き換えを再ステージする・環境問題は報告する)をCLAUDE.mdに書き、--no-verify・-n・HUSKY=0・SKIP=の迂回をPreToolUse hookのexit 2で止めます。huskyとpre-commitを併用するなら、入口を.husky/pre-commitに寄せて失敗出力を1本にすると、Claudeが原因を読み違えにくくなります。

この記事を共有:XはてブLinkedIn