Claude Code Hooksで検品を自動化する|保存後・完了前に何を走らせるか
Claude Code Hooksでlintやテストを自動実行する方法を、PostToolUseとStopの使い分け、最小設定、終了コード、タイムアウト、安全な運用まで解説します。
Claude Codeへ「最後にテストして」と頼んでも、長い作業では確認が抜けることがあります。Hooks(フック)を使うと、ファイル編集後や応答終了時など、決めたタイミングでコマンドを機械的に実行できます。言いかえると、頼み忘れても必ず出勤する「検品係」を置く仕組みです。
ただし、Hookは判断そのものを肩代わりする道具ではありません。決めた検査を忘れず起動する道具です。最初から自動修正やデプロイまで任せるのは危険です。まずはlint(コードの静的検査)とテストを読み取り中心で走らせ、失敗をClaudeへ返すところから始めます。
この記事の仕様の記述は、2026-07-26時点の公式ドキュメントで確認しました。Hooksは更新が続いている機能なので、導入するときはその時点の公式案内でも確認してください。
Hooksで自動化するのは「判断」ではなく「決めた検査の起動」
Hooksに向くのは、同じ入力なら同じ判定になる検品です。代表例は、フォーマット確認、型検査、lint、対象を絞った単体テスト、禁止文字列の検索です。公式のHooksリファレンスによると、Hookはツール実行の前後や応答終了時など、Claude Codeの決まった時点で自動実行され、そのイベントの文脈を表すJSON(機械が読む文字列)が標準入力で渡されます。つまりHookは「いつ・何を起動するか」を機械に固定する仕組みです。
一方で、「設計として自然か」「公開してよいか」のように判断が揺れる仕事は、固定コマンドだけで合否を決めない方が安全です。デプロイ、データ削除、全ファイルへの自動整形のような、副作用が大きい処理も最初のHookからは外します。GitHubの継続的インテグレーションの解説でも、lint・カバレッジ・機能テストのように「機械が繰り返し実行できる検査」を自動化の対象にしています。まずはこの層だけをHookへ載せます。
自動化の範囲を決める前に、毎日の作業を自動化する考え方で「繰り返す判定」と「人が決める判断」を分けておくと、Hookへ詰め込みすぎずに済みます。
先に検品コマンドを単独で合格させる
いきなり設定ファイルへ書くと、Hookが動かないのか、コマンドが失敗しているのか、切り分けができなくなります。順番はこうです。
package.jsonのscriptsに、lint・typecheck・testなど検品の入口を定義する- 端末から
npm run lintのように単独で実行し、成功したら終了コード(コマンドの合否を表す番号)0が返ることを確かめる - わざと小さな違反を作り、失敗したら終了コードが0以外になることを確かめる
- 1回の所要時間をストップウォッチで測る(Hookのタイムアウトを決める材料になる)
npmの公式ドキュメントによると、scriptsには任意のコマンドを定義でき、スクリプトはパッケージのルート(package.jsonのある場所)から実行され、依存パッケージの実行ファイルはPATHへ追加されます。0以外で終了するとnpmの処理は中止されます。ここで押さえたいのは、npm側の「0以外で失敗」と、後で出てくるClaude Code Hookの「終了コード2でブロック」は別物だという点です。混同すると、テストが落ちているのにClaudeがそのまま終わる事故が起きます。
検品ラッパー(別のコマンドを包む小さなスクリプト)を書くなら、子プロセス(呼び出した別のプログラム)の終了状態の扱いにも注意します。Node.jsの子プロセスのドキュメントによると、子プロセスが自分で終了した場合は終了コードが数値になり、シグナル(外から強制終了された合図)で終わった場合はnull(値なし)になります。さらに同ドキュメントは、シェルを有効にした実行へ未検証の入力を渡すと、シェルの特殊文字を使った任意コマンド実行につながり得ると警告しています。Hookの入力をそのままコマンドへつなげない、という設計の根拠になります。
PostToolUse・FileChanged・Stopを時間軸で選ぶ
発火点(Hookが動くタイミング)は、時間軸で選びます。ここで日常語の「保存後」を1つの発火点と同一視しないことが大事です。「保存後」に見える動作でも、公式では別々のイベントに分かれています。
| 発火点 | 動くタイミング | 検品の例 | 注意点 |
|---|---|---|---|
PreToolUse | ツールを実行する前 | 保護ファイルへの書き込み拒否 | 実行前に止めたい条件だけに絞る |
PostToolUse | ツールが正常に完了した後 | 対象ファイルのlint、形式確認 | 済んだ変更は取り消せない・Bash経由の変更は拾わない |
FileChanged | 監視ファイルがディスク上で変わった後 | 設定ファイル変更への通知 | 名指しファイルだけ・ブロック機能なし |
Stop | Claudeが応答を終えようとした時 | テスト一式、最終チェック | 応答のたびに動く・再発火の上限あり |
Hooksリファレンスによると、PostToolUseはツールが正常に完了した直後に動きますが、その操作はすでに実行済みなので、Hookの出力で変更そのものを取り消すことはできません。FileChangedは監視対象のファイルがディスク上で変わった時に動き、入力には絶対パスとchange・add・unlinkという種別が入ります。ただし監視するのは作業ディレクトリ内の「名指ししたファイル」だけで、FileChanged自体に変更を止める機能はありません。StopはメインのClaudeが応答を終えた時に動き、ユーザーの割り込みでは動きません。
迷ったら、数秒で終わる局所的な検査はPostToolUse、設定ファイルの変化への反応はFileChanged、作業全体に対する検査はStopへ分けます。Stopは「タスクが完成した時だけ」動くのではなく、途中経過の返事でも発火するので、重いテストを置くならこの性質を前提に待ち時間を見積もります。
PostToolUseで短いlintを返す最小構成
導入は2段階に分けます。まず自分だけの.claude/settings.local.jsonで試し、安定してからチーム共有の.claude/settings.jsonへ移します。全プロジェクトへ適用する個人設定は~/.claude/settings.jsonです。Hooksガイドによると、設定済みのHookと設定元は/hooksから一覧できます。
次の例は、EditまたはWriteツールの完了後にlintを実行する最小構成です。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npm run lint",
"timeout": 120
}
]
}
]
}
}
設定は「イベント」「matcher(対象を選ぶ条件)」「実行するコマンド」の3段です。timeoutの単位は秒で、この例では2分が上限です。command Hookの既定タイムアウトは秒単位で、2026-07-26時点のリファレンスでは600秒(10分)ですが、既定値を推奨値と考えず、検査ごとに所要時間を実測して短い上限を置いてください。仕様は変わりやすいので、導入前にその時点の公式案内で確認してください。
ここで大事な注意があります。Edit|Writeは「保存後の全変更」を拾いません。 このmatcherが一致するのはEditツールとWriteツールだけで、ClaudeはBashコマンドでもファイルを書き換えられます。Hooksガイドもこの取りこぼしに触れています。FileChangedを使えば特定ファイルのディスク変更は捕まえられますが、それは名指ししたファイルの監視であって、あらゆる変更を自動で拾う汎用の監査ではありません。全変更を確実に検品したいなら、Stopで作業ツリー全体(git diffの対象など)を一括検査する方が確実です。
matcherは配列ではなく1つの文字列で書き、Edit|Writeのように|で複数ツールをつなぎます。ツール名は大文字小文字を区別します。設定したら/hooksで、イベント・matcher・コマンド・設定元を確認します。
完了前テストをStopで合格まで返す
Stopの使いどころは「Claudeが終わろうとした時に、テストが通っていなければ終わらせない」検品です。ここには初心者が必ず踏む罠があります。
テストコマンドの失敗(終了コード1)を返しただけでは、Claudeは止まりません。Hooksリファレンスのとおり、終了コード0は成功、2はブロックで、多くのイベントでは2以外の非ゼロは「非ブロック」として扱われ、処理はそのまま進みます。テスト失敗でClaudeに修正を続けさせるには、次のどちらかが要ります。
- テスト失敗を終了コード2へ変換する小さなラッパー(包み紙のスクリプト)を書く
- 終了コード0で、標準出力へブロック用の構造化JSON(
decisionをblock、reasonを必須)だけを返す
ラッパーの骨組みは次のとおりです(bashの例)。
#!/bin/bash
input=$(cat)
# すでにStop Hookで継続中なら、再ブロックしない(無限ループ防止)
if echo "$input" | grep -q '"stop_hook_active":true'; then
exit 0
fi
if npm test 1>&2; then
exit 0
else
echo "テストが失敗しています。失敗ログを読んで修正してください" 1>&2
exit 2
fi
終了コード2のときは、標準エラー出力の文章が理由としてClaudeへ返ります。JSON方式を選ぶ場合は、標準出力をJSONオブジェクトだけにし、診断の文章は標準エラーへ分けます。終了コード2とJSON方式を同時に使わないでください。 混ぜると解釈に失敗します。終了コード2で返すときはJSONが無視されます。
再発火の制御も設計に入れます。入力のstop_hook_activeがtrueなら、Stop Hookの結果としてすでに会話が継続中です。Hooksリファレンスによると、Claude Codeは進展のないままStop Hookが8回連続で継続すると、そのHookを上書きしてターンを終了します。つまり「合格するまで無限に粘る」設計は、そもそも成立しません。回数内で直せる粒度の検査に絞り、解消できない条件で毎回ブロックし続けないようにします。
Windowsで終了コードの動きを手元から確かめる方法は、PowerShell・コマンドプロンプト・GitHub Actionsにおける終了コードの違いで整理しています。
安全に運用し、CIと二段構えにする
Hookのコマンドは、あなたのOSユーザーの権限でそのまま実行されます。Hooksリファレンスのとおり、そのユーザーが触れるファイルは変更も削除も読み取りもできます。だから次を守ります。
- 外部からコピーした設定は、中身を読んで理解してから使う
- 最初は読み取り中心の検査に限定する。自動修正(
--fix)を使うなら対象を絞る - 秘密(
.env、鍵、.gitの内部)を読む処理を入れない。共有設定へAPIキーを直書きしない - スクリプトでは入力を検証し、変数を引用符で囲み、
..によるパス移動を防ぎ、絶対パスを使う
設計上の注意も2つあります。第一に、同じイベントに一致した複数のHookは並列(同時)に実行され、同一の処理は自動で重複排除されます。1つのHookのブロックは兄弟Hookの実行を止めないので、「Aが拒否すればBは動かない」という順序頼みの設計は成り立ちません。第二に、Windowsでは"shell": "powershell"を指定でき、pwsh.exeが自動検出され、なければpowershell.exeが使われます。一方でnpmのscriptsの既定シェルは、Windowsではcmd.exe、POSIX(Mac・Linux)では/bin/shです。PowerShell・cmd.exe・bashで引用符や環境変数の書き方が違うので、記事や設定例がどのシェル向けかを必ず意識します。複雑な処理は短いスクリプトファイルへ分離すると、引用符の事故が減ります。
まとめて止めたいときは、設定で次のようにします。
{ "disableAllHooks": true }
これで、管理者ポリシーで強制されたものを除くHookをまとめて無効化できます。個別Hookだけを設定に残したまま一時停止する機能はないため、外すときは差分を控えてから該当設定を削除します。
そして、HooksはCI(共有サーバーでの自動検査)の代わりにはなりません。GitHubのCI解説によると、CIはコード変更に対してビルドやテストを継続的に実行し、結果をプルリクエスト(変更の取り込み依頼)へ表示できます。Hooksは作業中の手元フィードバック、CIは共有された変更の再検査と結果の記録、という役割分担です。両方から同じpackage.jsonのscriptsを呼ぶと、検査内容を揃えられます。検品項目そのものの見直しにはAI生成コードの検品チェックリストが使えます。
よくある質問
PostToolUseとFileChangedはどう使い分けますか?
EditやWriteなど特定ツールの正常完了後に検査するならPostToolUse、名前を指定した監視ファイルのディスク変更へ反応するならFileChangedです。FileChangedは全ファイル変更を自動で拾う汎用の監査ではなく、ブロック機能もありません。全変更の検査が目的ならStopでまとめて走査する方が確実です。
テストが終了コード1でもClaudeが完了してしまうのはなぜですか?
npm側では0以外の終了が処理失敗になりますが、Claude Codeの多くのHookイベントでは終了コード2がブロックを表すからです。Stopを拒否するなら、テストの失敗を2へ変換するラッパーを挟むか、終了コード0で構造化JSONのblock理由を返します。
Windowsでも同じ設定を使えますか?
使えますが、シェルの違いに注意します。Hookにはshell: powershellを指定できますが、npmのscriptsの既定シェルはWindowsではcmd.exe、POSIXでは/bin/shです。引用符・環境変数・パス区切りを混ぜず、複雑な検品はOS差を吸収するスクリプトへ分けます。
Stop Hookが何度も繰り返されると無限に続きますか?
続きません。入力のstop_hook_activeで継続中かを判別でき、Claude Codeは8回連続で継続するとHookを上書きしてターンを終了します。回数頼みにせず、再実行で直せる検査に絞り、解消できないならブロックを解除する設計にします。
HooksがあればGitHub Actionsは不要ですか?
不要にはなりません。Hooksは作業中の手元フィードバックに向き、CIは共有リポジトリの変更に対して同じビルドやテストを再実行し、プルリクエストへ結果を残せます。両方で同じpackage.jsonのscriptsを呼ぶと、検査内容を揃えやすくなります。
まとめ
短い検査はPostToolUse、特定ファイルの監視はFileChanged、作業全体の検査はStop、実行前の拒否だけをPreToolUseへ。この役割分担が崩れなければ、Hooksは「頼み忘れても走る検品係」として安定して働きます。次にやることは3つです。
- まず
package.jsonのscriptsにlintを固定し、端末で成功例と意図的失敗例・所要時間を確かめてから、.claude/settings.local.jsonへPostToolUseで1本だけ入れて試す - わざと失敗を作り、失敗がClaudeへ返ること・
/hooksで発火が見えること・disableAllHooksで外して戻せることを確認する - 安定したら
.claude/settings.jsonへ移し、Stopの完了前テストを「終了コード2への変換」とstop_hook_activeの確認つきで足し、同じscriptsをGitHub ActionsのCIでも走らせる
仕様の細部(既定タイムアウト、Stopのブロック上限、matcherの書式、PowerShellの扱い)は今後も変わりえます。導入と公開の前に、その時点の公式ドキュメントで確認してください。
公式一次情報
確認日: 2026-07-26
- Hooks reference - Claude Code Docs: イベント一覧、matcher、入力JSON、終了コードと構造化JSON、タイムアウト、Stopの8回上限、FileChanged、Windows対応、安全上の注意が載っています
- Automate workflows with hooks - Claude Code Docs: 導入例、
Edit|Writeの対象範囲、複数Hookの並列実行、/hooksでの確認、終了コードとJSONを混ぜない注意が載っています - Child process - Node.js Documentation: 子プロセスの終了コードとシグナル時のnull、シェル有効時に未検証入力を渡す危険が載っています
- Scripts - npm Docs:
package.jsonのscripts定義、npm run、POSIXとWindowsの既定シェルの違い、0以外終了で処理中止が載っています - Continuous integration - GitHub Docs: CIで変更ごとにビルドとテストを再実行し、lintやカバレッジを含め、結果をプルリクエストへ残せることが載っています
続き(結論と実データ)はnoteに置いています
ここでは手順のところまで書きました。実際に出た数字、うまくいかなかった条件、そのまま使える設定ファイルは、 note の記事にまとめてあります。