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つの部品でできています。
Gitフックが落ちたときの2つの部品
お願い
失敗した出力を読んで原因を直し、コミットをやり直すよう指示します。CLAUDE.mdはコンテキストであって強制設定ではない、とメモリの解説に明記されています。
強制
フックを飛ばして通す近道を、実行前に物理的に塞ぎます。ブロックしたい操作はPreToolUse hookを使う、という切り分けです。
Claude側のhookでLintを先回りして直す設計は、PostToolUse hookのLint自動修正が扱っています。ここでは、それでも取りこぼした指摘がGitフックで落ちたあとの流れを組みます。コミットメッセージの書式を縛りたい場合は、commitlintで規約違反を差し戻す方法も使えます。
huskyとpre-commitは何が違うのか
どちらもGitフックを管理する道具ですが、持っている責務が違います。
| 項目 | husky | pre-commit |
|---|---|---|
| フックの置き場 | husky.husky/pre-commitなどのスクリプト | pre-commit.pre-commit-config.yamlに宣言 |
| 中身の書き方 | huskyシェルスクリプトを直接書く | pre-commit公開リポジトリのhookをidで指定 |
| 導入コマンド | huskynpx husky init | pre-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どちらの公式手順にもない組み合わせです。
# .husky/pre-commit
pre-commit runpre-commit runは、ステージ済みの変更に対して設定済みのhookを実行します。リポジトリ全体に走らせたいときはpre-commit run --all-filesです。新しいhookを足したときは、まず全ファイルへ走らせておくのが通例です。
既存のGitフックがある環境でpre-commit installすると、既存のフックとpre-commitのフックを両方走らせる移行モードで入る、とpre-commitのドキュメントにあります。上書きしたい場合は-f(--overwrite)を渡します。フックに載せるテストの絞り方はClaude CodeでRSpecを回す記事にあります。
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 code | Claudeの動き指摘箇所を直し、再ステージして再コミット |
| 自動修正hookがファイルを書き換えた | 出力の手がかりFiles were modified by this hook | Claudeの動き変更を確認してステージし直し、再コミット |
| 環境の問題 | 出力の手がかり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にとっては「テストを通す近道」にもなります。
HUSKY=0は、huskyを経由しない素のフックには効きません。一時リポジトリに素のシェルスクリプトをcore.hooksPathで登録し、HUSKY=0 git commitを試すと、フックはそのまま走ってexit 1でした。同じ一時リポジトリでgit commit -nと-nmは、どちらもフックを飛ばして通りました。
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')
# 引用符の中(コミットメッセージなど)を取り除いてから判定する
BARE=$(echo "$CMD" | sed -E "s/\"[^\"]*\"//g; s/'[^']*'//g")
# git -c ... commit のようにオプションを挟んだ形も対象にする
GIT='(^|[[:space:];&|])git([[:space:]]+(-c|-C)[[:space:]]+[^[:space:]]+)*[[:space:]]+(commit|push)'
if ! echo "$BARE" | grep -Eq "$GIT"; then
exit 0
fi
if echo "$BARE" | grep -Eq '(--no-verify|[[:space:]]-[a-zA-Z]*n[a-zA-Z]*([[:space:]]|$)|HUSKY=0|SKIP=|core\.hooksPath)'; then
echo "Blocked: Gitフックを飛ばす操作は禁止です。フックの出力を読んで原因を直してください。" >&2
exit 2
fi
exit 0-nはgit commitではフックを飛ばす短縮形で、-nmのようにほかの短いフラグと束ねられることもあります。そのため正規表現で束ねた形も拾っています。一方でgit push -nは--dry-runの意味ですが、このスクリプトは止めます。前段の判定は、ls -nのようにgit commitとgit push以外のコマンドを対象外にするためのものです。
このスクリプトは一時ディレクトリで検証しました。jqでコマンド文字列をJSONに包んで標準入力に流し、終了コードを見ています(git 2.50.1)。素朴にgit commitとgit pushの連続や-nなどだけを見る版には、穴と誤検知がありました。
素朴な版で起きた不具合
止めるべきなのに素通り
1形
git -c core.hooksPath=/dev/null commit
止めなくてよいのに停止
2形
メッセージ中の -n や --no-verify
一時リポジトリでgit -c core.hooksPath=/dev/null commitを実行すると、exit 1で落ちるはずのフックが走らず、コミットが通りました。gitとcommitの間に-cが入ると、git commitの連続を前提にした判定をすり抜けます。上のスクリプトは、オプションを挟んだ形も対象にする正規表現に直してあります。
git commit -am "fix: -n option"とgit commit -m "wip --no-verify"は、メッセージ中の文字列に反応して止まりました。引用符の中を取り除いてから判定すると解消します。なお、スクリプトが見るのは同じコマンド内のHUSKY=0です。huskyのHow Toには、~/.config/husky/init.shにexport HUSKY=0と書くとそのマシンでフックが動かなくなる、という記載もあります。別コマンドでのexportやこのファイルの編集は、上のスクリプトでは止まりません。
登録は.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.shhookのifフィールド("if": "Bash(git *)"など)でhookの起動を絞ることもできます。ただしこの絞り込みはベストエフォートで、確実な許可・拒否にはhookでなく権限システムを使うよう、公式ガイドは案内しています。ifは前置のVAR=valueを除いて照合しますが、git -c ... commitのようにオプションを挟んだ形などは絞り込みから漏れるため、スクリプト側でコマンド全体を検査します。
権限のdenyルールを併用する
権限側でも、拒否ルールを足せます。denyはallowより優先され、*はルールのどの位置にも置けます。
{
"permissions": {
"deny": [
"Bash(git commit *--no-verify*)",
"Bash(git push *--no-verify*)"
]
}
}-nの束ね書きやgit -c core.hooksPath=/dev/null commitのようにオプションを挟んだ形は、この形では拾えません。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に直させる用途では、指摘がまとまって出る既定のほうが往復が少なく済みます。 - index.lockでコミットが落ちる: Gitフックではなくロックファイルの残りが原因なら、Claude Codeでindex.lockが残るときの対処で確認と削除の手順を扱っています。
まとめ
迂回の封じ方は、denyルールでは拾えない書き方が残るため、PreToolUse hookを主にしてdenyは保険に回す形が堅実です。command not foundのような環境の失敗はClaudeに直させず、人がinit.shなどで直す領域として切り分けます。