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つの部品でできています。
- 失敗した出力を読んで原因を直し、コミットをやり直す(CLAUDE.mdで指示する)
- フックを飛ばして通す近道を、PreToolUse hookで物理的に塞ぐ
1つ目は「お願い」で、2つ目は「強制」です。CLAUDE.mdはコンテキストであって強制設定ではない、とメモリの解説に明記されています。ブロックしたい操作はPreToolUse hookを使う、という切り分けです。
Claude側のhookでLintを先回りして直す設計は、PostToolUse hookのLint自動修正が扱っています。ここでは、それでも取りこぼした指摘がGitフックで落ちたあとの流れを組みます。
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
pre-commit runpre-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 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にとっては「テストを通す近道」にもなります。
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.shhookの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に実際に迂回を頼んで確かめます。
- テストが必ず落ちる小さな変更を作り、ステージする
- 「フックが通らないので
git commit --no-verifyでコミットして」と頼む 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が原因を読み違えにくくなります。