Claude Codeのフック2026:段階的な設定方法とすぐ使える7つの例
Markdownコピーしてすぐ使える .claude/settings.json 用のフック7種:.env の保護、prettier、main への force-push、em dash、開発サーバー。4種類のハンドラーと31個のイベント。
記事の全文を読む
Claude Codeのフックは、Claudeのアクションの前後に自動実行されるシェルコマンド、HTTP呼び出し、または短いLLMチェックです。CLAUDE.mdではできないことを担います。特定ツールイベントの検査、編集後の整形、開始時の文脈追加を行います。一致するPreToolUseはその呼び出しを拒否できますが、正規表現の例は利便的なガードで、全ファイル・Git経路を覆う安全境界ではありません。以下では、4種類のハンドラー、31個のライフサイクルイベント、「ルールをフックにするべきタイミング」のテスト、.claude/settings.jsonにコピーして使える7つのフック、そして私自身が経験した3つの落とし穴を解説します。
Claude Codeのフックとは?
重要なポイント
フックとは、特定のライフサイクルイベントに応じてClaude Codeが実行するシェルコマンド、HTTP呼び出し、短いLLMプロンプト、またはサブエージェントです。モデルが無視したり忘れたりする可能性のあるCLAUDE.mdの指示とは異なり、フックは毎回同じように実行されます。提案ではなく、コードです。
半年間、私は「.envを直接編集しない」というルールをCLAUDE.mdに書いていました。Claudeは月に一度それを破りました。悪意があったのではなく、長いセッションの終わりには単に忘れてしまったのです。あるときは余計なトークンが.envに入り、別のときは同じ名前の変数が使われていた変数を上書きしました。
直接の編集ツールが.envパスを渡し、条件に一致した場合に、その呼び出しを拒否します。Bashのリダイレクト、スクリプト、MCP、別経路には独立した制御が必要で、ファイルを変更不能にする例ではありません。
フックは、定義したシェルコマンド、HTTPエンドポイント、またはLLMプロンプトで、Claude Codeのライフサイクル内の特定のタイミングに自動実行されます。
言い換えれば、フックとは、特定のイベントでClaude Codeが実行するあなたのコマンドです。ファイル編集、Bashの起動、セッションの開始、サブエージェントの完了など、あらゆるイベントにフックを設定できます。
vibe coderにとって、これが重要な理由は次のとおりです。
- シークレットを含むファイルを誤編集から保護する。
- コード生成のたびにprettier/eslintを自動実行する。
- 明示的な許可なしに
git push --forceをmainへ実行する操作をブロックする。 - 各セッションの開始時に新しいコンテキストを注入する。
- Claudeの操作を監査用に記録する。
CLAUDE.mdはコンテキストを扱います。フックはアクションを扱います。この2つは別物であり、混同することが初心者の主な間違いです。
フックはCLAUDE.mdやスキルとどう違う?
重要なポイント
CLAUDE.mdは、セッションの開始時にモデルが読み、従おうとするテキストです。フックは、モデルとは独立して、イベントに応じてClaude Codeのプロセスが実行するシェルコマンドです。スキルは、関数のようにClaudeがSkillツールで呼び出せる、名前付きの指示パッケージです。3つはそれぞれ異なる種類のタスクを扱います。
Anthropicのドキュメントでは、CLAUDE.mdとフックの違いが明確に説明されています。
CLAUDE.mdの内容はシステムプロンプトの一部としてではなく、システムプロンプトの後にユーザーメッセージとして渡されます。Claudeはそれを読み、従おうとしますが、厳密に従う保証はありません。
別の言い方をすれば、CLAUDE.mdの内容はシステム指示としてではなく、ユーザーからの最初のメッセージとしてClaudeに届きます。モデルはそれを読み、従おうとします。しかし、特に長いセッションの終盤では保証はありません。コンテキストが膨らみ、古い指示が押し出されるからです。
フックの仕組みは異なります。フックはsettings.jsonにあり、モデルに依存せず、PreToolUse、PostToolUse、SessionStartなどの特定のイベントでClaude Codeのプロセスが実行します。コマンドはClaudeが結果を受け取る前に実行され、一致するPreToolUseを拒否(exit 2)し、編集後のPostToolUseでは検査や整形を行えます。後者は完了済みの編集を阻止・取消するものではありません。
比較:
| 特徴 | CLAUDE.md | フック | スキル |
|---|---|---|---|
| 保存場所 | プロジェクトルートのCLAUDE.md | .claude/settings.json | .claude/skills/フォルダー |
| 実行するもの | モデルがユーザーメッセージの一部として読む | イベント発生時にClaude Codeのプロセスが実行 | Skillツールを通じてモデルが実行 |
| 実行の保証 | なし(ベストエフォート) | 設定が一致したコマンドを実行。失敗と終了動作はイベントによる | モデル呼び出しを通じて(許可はあるが、シェルレベルではない) |
| 用途 | プロジェクトのコンテキスト、トーン、スタイル、ルーティング | 制約、整形、特定ファイルのチェック | パラメーター付きの複雑な複数ステップ手順 |
| 実行タイミング | セッション中のすべてのモデルメッセージ | ライフサイクルイベント時 | モデルが呼び出したとき |
| アクションをブロックできるか | いいえ | 一致するPreToolUseを拒否。PostToolUseは実行後の報告 | いいえ |
CLAUDE.mdの完全なテンプレートと、ファイルを育てるための6つのルールは、ガイド2026年版CLAUDE.mdの設定方法:すぐ使えるテンプレートと6つのルールで紹介しています。名前付き機能としてのスキルについては、Claude Code Skills:独自のライブラリーを作る方法をご覧ください。
フックは最も低いレベルにあります。モデルも解釈もなく、シェルだけです。
フックは、Plan Modeや/compactと並ぶコンテキストエンジニアリングの手段の1つです。
4種類のフック:Command、HTTP、Prompt、Agent——どれを選ぶ?
重要なポイント
Claude Codeには4種類のハンドラーがあります。Commandは最も一般的なシェルスクリプトです。HTTPはURLへPOSTリクエストを送り、チームや連携向けです。Promptは短いHaiku呼び出しで意味を確認します。Agentはツールにアクセスできるサブエージェントを作成し、詳細な検証を行います。初心者にはCommandとPromptがあれば十分で、タスクの90%をカバーします。
| 種類 | 機能 | 選ぶ場面 | 複雑さ |
|---|---|---|---|
| Command | シェルコマンドを実行し、stdinからJSONを受け取り、終了コードとstdoutで応答 | 90%のケース:パスのチェック、整形、コンテンツのgrep、lsof、Git操作 | 低 |
| HTTP | JSONボディを添えてURLへPOSTリクエストを送り、レスポンスでJSONを受け取る | ルールをチームやCIに置く場合:一元的な検証、共有ステータスダッシュボード、リモート監査 | 中(サーバーが必要) |
| Prompt | 短いプロンプトを(デフォルトで)Haikuに送り、意味的な評価についてyes/noを受け取る | grepでは不十分な場合:「この文章が企業的な響きか評価する」 | 低 |
| Agent | Read/Grep/Globにアクセスできるサブエージェントを作成し、複数ステップのチェックを行う | 大がかりな検証:「変更されたすべてのファイルを見つけ、対応するテストがあるか確認する」 | 高 |
よくある間違いは、Promptが必要な場面でCommandを使おうとすることです。たとえば「.mdファイルではem dashを使わない」はgrepで確認できるので、Commandが最適です。しかし「企業的なトーンで書かない」は意味に関する判断なので、Promptが必要です。
始めたばかりなら、Commandを使いましょう。以下のすぐ使える7つのフックは、すべてCommandです。HTTP/Prompt/Agentは、準備ができてから使えばよいでしょう。
settings.jsonはどこにあり、設定レベルは4つある?
重要なポイント
Claude Codeは4か所の設定を参照します。エンタープライズ(MDM経由で展開)、ユーザー(~/.claude/settings.json)、プロジェクト(リポジトリルートの.claude/settings.json)、ローカル(.claude/settings.local.json、Git対象外)です。4つのファイルはすべてセッション開始時に統合され、下位レベルの設定が優先されます。
| レベル | パス | 適用範囲 | Gitに含まれるか |
|---|---|---|---|
| エンタープライズ | /Library/Application Support/ClaudeCode/settings.json(Mac)、/etc/claude-code/settings.json(Linux) | MDM経由でチーム全体に適用 | いいえ |
| ユーザー | ~/.claude/settings.json | 1人のユーザーのすべてのプロジェクト | いいえ |
| プロジェクト | リポジトリルートの.claude/settings.json | チーム全体のこのプロジェクト | はい |
| ローカル | .claude/settings.local.json | このユーザーのこのプロジェクトのみ | いいえ(.gitignore内) |
以下で説明する7つのフックは、プロジェクトの.claude/settings.jsonにまとめています。リポジトリをクローンすると自動的に含まれ、新しいウィンドウでもClaudeが同じルールに従うからです。個人的な実験用フックは.claude/settings.local.jsonに置き、チームの妨げにならないようにします。
4つのファイルはすべてセッション開始時に統合されます。ルールが競合する場合は、下位のレベルが優先されます(ローカルがプロジェクトより優先され、プロジェクトがユーザーより優先されます)。
最初のフックを最も手早く作成する方法は、Claude Code内の対話式/hooksメニューです。リストからイベントを選び、マッチャー(たとえばBashやEdit)を選択して、コマンドを貼り付けます。Claude Codeがsettings.jsonに有効なJSONを書き込みます。
31個のライフサイクルイベント:初心者にはどれが必要?
重要なポイント
Claude Codeには現在31個のライフサイクルイベントがあり、3つの頻度に分けられます。セッションごとに1回(SessionStart/SessionEnd)、ターンごとに1回(UserPromptSubmit/Stop)、ツール呼び出しごと(PreToolUse/PostToolUse)です。初心者に必要なのは31個のうち4つです。制約にはPreToolUse、自動編集にはPostToolUse、コンテキストにはSessionStart、最終チェックにはStopを使います。
イベントは3つの頻度で発生します。セッションごとに1回(SessionStart、SessionEnd)、ターンごとに1回(UserPromptSubmit、Stop、StopFailure)、エージェントループ内の各ツール呼び出しで(PreToolUse、PostToolUse)発生します。
3つの頻度を理解しておくと、混乱を避けられます。
- セッションごとに1回 -
SessionStart、SessionEnd、Setup。Claude Codeを開いたときと閉じたときに1回実行されます。コンテキストの初期化や一時ファイルの整理に使います。 - ターンごとに1回 -
UserPromptSubmit、UserPromptExpansion、Stop、StopFailure。あなたがメッセージを送るたび、またモデルが停止するたびに発火します。会話の記録や最終結果のチェックに使います。 - ツール呼び出しごと -
PreToolUse、PostToolUse、PostToolUseFailure、PermissionRequest、PermissionDenied、PostToolBatch、SubagentStart、SubagentStop。ファイル編集、Bashの起動、読み取り、検索のたびに発火します。制約、整形、検証を行う、最も頻繁に実行される層です。
31個のイベントの全リストには、非同期・リアクティブなイベントも含まれます。TeammateIdle、PreCompact、PostCompact、Elicitation、ElicitationResult、InstructionsLoaded、ConfigChange、CwdChanged、FileChanged、WorktreeCreate、WorktreeRemove、Notificationです。これらは高度な用途向けなので、今は飛ばしましょう。
5つの質問テスト:ルールをフックにするのはいつ?CLAUDE.mdに残すのはいつ?
重要なポイント
ルールがコンテキストではなくアクションについてのもので、安全・金銭・データに関わり、grep/regexでチェックでき、破ると取り消しが難しく、Claudeがすでに少なくとも1回破ったことがあるなら、フックにするべきです。この5項目のうち「はい」が3つ未満なら、CLAUDE.mdに残します。私は80行のルールにこのテストを適用し、5つをフックに移しました。
私にとって、これは提案ではなく法律のようなものです。CLAUDE.mdとフックのどちらにするかを、簡単な5つの質問にまとめました。「はい」が3つ以上なら、そのルールはフックにします。3つ未満なら、テキストに残します。
「ルールをフックにするのはいつ?」のテスト:
- このルールはコンテキストではなく、アクションについてのものですか?(「
rm -rfを実行しない」はアクション、「TypeScriptのプロジェクトがある」はコンテキストです。) - このルールは安全、金銭、またはデータに関するものですか?(「
.envに触らない」「キーを公開しない」。) - grep、regex、またはシェルコマンドの終了コードでチェックできますか?(「
.mdを編集した後、em dashがないか確認する」。) - ルールを破った場合、元に戻すのが大変な結果になりますか?(「mainへのforce push」は元に戻すのが難しく、「文末のピリオドを忘れた」は簡単です。)
- Claudeがこのルールを少なくとも一度忘れるのを、すでに見たことがありますか?
例:「em dashを使わず、ハイフンだけを使う。」 アクション?はい。安全?いいえ。grepで確認できる?はい。元に戻すのが難しい?いいえ。違反を見たことがある?はい。5つ中3つ:フックにする。
反例:「私は開発者ではなくvibe coderなので、スラングを使わずに書いて。」 アクション?どちらかといえばコンテキスト。安全?いいえ。regexで確認できる?難しすぎます。元に戻すのが難しい?いいえ。違反を見たことがある?はい。5つ中1つ:CLAUDE.mdに残す。
私のルールのうち5つがテストに合格し、フックに移りました。残りの75%はテキストファイルに残しました。
コピーして使えるフック7選
重要なポイント
以下は、.claude/settings.jsonにそのまま使える7つのフックです。.envの編集をブロックする、prettierを自動実行する、mainへのforce-pushを防ぐ、em dashをチェックする、npm run devの前にポートを解放する、テスト成功後に自動コミットする、セッション開始時に新しいコンテキストを注入する、という内容です。それぞれをファイルにコピーしてClaude Codeを再起動すれば動きます。
7つのフックはすべて、プロジェクトルートの1つの.claude/settings.jsonファイルに保存します。それぞれのエントリーには、イベントの種類、ツールフィルター(マッチャー)、シェルコマンドがあります。終了コード2は、一致するPreToolUseの実行前呼び出しを拒否します。PostToolUseでは操作済みなので、フィードバックを伝えるだけで取り消せません。
ほとんどのフックイベントでは、アクションをブロックするのは終了コード2だけです。Claude Codeは終了コード1をブロックしないエラーとして扱い、Unixでは通常失敗を意味するにもかかわらず、アクションを続行します。
exit 0は成功を意味します。exit 1はエラーを意味しますが、Claudeは続行します(以下の最初のアンチパターンです)。exit 2はイベント依存です。PreToolUseを拒否できますが、PostToolUseは実行済み操作へのフィードバックです。
フック1 直接の.env編集を検査する(PreToolUse、exit 2)
.env、.env.local、.env.productionなどに一致するファイルを直接編集しようとする操作をブロックします。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '(^|/)\\.env(\\.|$)' && { echo 'Do not edit `.env` directly. Tell me which variable to add, and I’ll open the file myself.' >&2; exit 2; } || exit 0"
}
]
}
]
}
}
動作: すべてのEdit/Write/MultiEditの前に、ファイルパスを取得し、grepで.envのパターンに一致するか確認します。一致する場合は、メッセージとともに終了コード2を返します。Claudeはstderrを見て、こう伝えます。「.envを編集しようとしましたが、フックにブロックされました。追加する変数を教えてください。」
フック2 コード編集のたびにprettierを自動実行する(PostToolUse、exit 0)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(ts|tsx|js|jsx|json)$' && npx prettier --write \"$FILE\" 2>/dev/null & exit 0"
}
]
}
]
}
}
動作: Edit/Writeの後、パスを取得し、拡張子が.ts/.tsx/.js/.jsx/.jsonなら、バックグラウンドで&を使ってprettierを実行します。これは制約ではなく整形なので、常に終了コード0を返します。prettierが失敗しても大きな問題ではなく、ファイルはそのままです。
フック1との違いは根本的です。PreToolUse + exit 2はアクションの前にハードブロックします。PostToolUse + exit 0はアクションの後にソフト編集します。目的が異なる2つのモードです。
フック3 mainブランチへのgit push --forceをブロックする(PreToolUse、exit 2)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE 'git[[:space:]]+push' && echo \"$CMD\" | grep -qE '(\\-\\-force|\\-\\-force-with-lease|[[:space:]]-f([[:space:]]|$))' && echo \"$CMD\" | grep -qE '([[:space:]/:])main([[:space:]/:]|$)' && { echo 'Force push to main is forbidden. Create a new branch or ask me to do it manually.' >&2; exit 2; } || exit 0"
}
]
}
]
}
}
動作: Bashを起動するたびに、コマンドにpushが含まれ、--force/--force-with-lease/-fが含まれ、mainに触れているかを確認します。3つすべてに一致すると、明確なメッセージとともにブロックします。
以前はこのルールをCLAUDE.mdに大文字で書いていました。それでもClaudeは、「悪いコミットを取り消すためにforce pushして」と頼むと、約2週間に1度は破りました。このフックは示した文字列表記を検出しますが、別名、スクリプト、暗黙の宛先ブランチ、別Gitクライアントは通り抜けえます。リモートでmainを保護し、回避可能な認証情報をエージェントへ与えないでください。
フック4 em dashを編集後に検出する(PostToolUse、exit 2)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(md|mdx|tsx?)$' && grep -q $'\\xe2\\x80\\x94' \"$FILE\" && { echo \"File $FILE contains an em dash (U+2014). Replace it with a regular hyphen (U+002D).\" >&2; exit 2; } || exit 0"
}
]
}
]
}
}
動作: PostToolUseのexit 2は編集後の検査失敗をClaudeへ伝えます。書き込みを防いだり元に戻したりはしません。Claudeがファイルを編集してem dashを残した場合、フックが失敗し、Claudeにフィードバックが返され、Claudeは再試行します。2〜3セッション後には、Claudeはこの文字をまったく挿入しなくなります。
grepが$'\xe2\x80\x94'を使ってem dash文字(U+2014)だけを検索している点に注意してください。これはUTF-8のバイトとして文字を書くBash表記です。単にgrep -q '-'と書くと、通常のハイフンまで検出し、すべてのファイルで実行されます。見えない文字がロジック全体を壊します。
フック5 npm run devの前にポート3000を解放する(PreToolUse、exit 0)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE '(npm|pnpm|yarn)[[:space:]]+run[[:space:]]+dev' && { lsof -ti:3000 | xargs -r kill -9 2>/dev/null; echo 'Port 3000 freed before launch' >&2; }; exit 0"
}
]
}
]
}
}
動作: npm run dev(またはpnpm/yarn)の前に、ポート3000を使っているプロセスを終了します。準備であり制約ではないため、常に終了コード0を返します。Claudeが「port in use」を見てコードの問題だと勘違いして修正を始めることがありますが、実際には古いサーバーが動き続けていただけ、という種類の罠を防ぎます。
フック6 テスト成功後に自動コミットする(PostToolUse、exit 0)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "INPUT=$(cat); CMD=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.command // empty') || exit 1; EXIT=$(printf '%s' \"$INPUT\" | jq -r '.tool_response.exit_code // empty') || exit 1; case \"$CMD\" in 'npm test'|'pnpm test'|'yarn test') ;; *) exit 0 ;; esac; [ \"$EXIT\" = \"0\" ] || exit 0; git diff --cached --quiet || { echo 'Existing staged changes: review manually' >&2; exit 0; }; git add -- src/orders.ts tests/orders.test.ts || exit 1; git diff --cached --quiet && exit 0; git commit -m 'tests: checkpoint after successful run'"
}
]
}
]
}
}
動作: JSONを1回読み、指定した単純なテストコマンドだけを受け付けます。tool_response.exit_codeが明示的に0の場合だけ続け、導入版に項目がなければ成功と推測せず飛ばします。src/orders.tsとtests/orders.test.tsを担当の正確なファイルへ変えてください。既存ステージ変更があれば止め、add失敗では中断し、空差分はコミットしません。通常のコミットフックは有効です。有効化前に、成功、失敗、結果欠落の入力をオフラインで試します。
アンチパターン:すべてのEditにgit commit --no-verifyを付けること。コミット検査は有効に保ちます。再帰起動があればトリガーを絞るか検査を設計し直し、既定で検証を回避しません。自動コミットの前に、ステージ差分と担当範囲を確認してください。
フック7 セッション開始時に新しいコンテキストを注入する(additionalContext付きSessionStart)
{
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo 'no-branch'); STATUS=$(git status --short 2>/dev/null | head -10); echo \"{\\\"additionalContext\\\":\\\"Current branch: $BRANCH. Uncommitted changes:\\n$STATUS\\\"}\""
}
]
}
]
}
}
動作: セッション開始時に、Gitブランチと未コミット変更の一覧を取得し、additionalContextフィールドを含むJSONを返します。Claude Codeはこのテキストを最初のシステムメッセージに注入するため、Claudeは最初から現在の場所と前回のセッションから残っているものを把握できます。
additionalContextは、環境の現在の状態や、直前に完了した操作についてClaudeが知っておくべき情報に使います。環境の状態(現在のブランチ、デプロイ先、アクティブなフィーチャーフラグ)、条件付きのプロジェクトルール(編集したファイルに適用するテストコマンド)、外部データ(自分に割り当てられた未解決の課題、最近のCI結果)などです。
このフックは、最新コミットとその説明、GitHub Actionsのステータス、Linear/Jiraのアクティブなスプリント、現在のリポジトリの未解決課題を取得するように拡張できます。
2人に1人が陥るフックの落とし穴3つ
重要なポイント
1つ目の落とし穴は、exit 1のフックが何も通知せずに通過し、Claudeがエラーを無視することです。2つ目は、フック内の重い処理が60秒のタイムアウトで終了させられることです。3つ目は、フックから見える環境変数が~/.zshrcのものだけで、プロジェクトの.envは見えないことです。どれも1行で解決できますが、知らないと「フックが動かない」と感じることがあります。
落とし穴1 exit 1のフックは何も通知せずに通過する
最も危険な落とし穴です。1週間、.env保護フックが機能していると思っていましたが、実際にはnullをチェックせずにjq -r '.tool_input.file_path'を使っていました。tool_inputが空のとき、jqはコード1で終了しました。Claudeはexit 1を「フックのエラーなので続行」と扱い、アクションを実行しました。なぜ.envがまだ編集されているのか理解できませんでした。入力を空にしてターミナルでフックを手動実行したとき、ようやく原因がわかりました。
修正は1つのルールです。 すべてのフックはnullのチェックから始め、|| exit 0で終えます。予期しない終了があっても、何も通知されずに通過せず「安全な」終了になります。
# Safe start and end template:
FILE=$(jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
# ... your logic ...
exit 0 # or || exit 0 on one line
落とし穴2 遅いフックはタイムアウトで終了させられる
Editの後にファイル全体へeslint --fixを実行するフックを作りました。大きなファイル(1000行以上)の処理には8〜10秒かかりました。Claude Codeのデフォルトのフックタイムアウトは60秒なので、制限内だと思っていました。しかし1分間に複数のEditが行われると、フックが積み重なってキューに入り、一部は単純にスキップされます。「hook timed out」とログに表示されて初めて気づきました。
修正: フックで重い処理が必要なら、バックグラウンドで実行します。結果を待たずにeslint --fix "$FILE" &とします。すると実際の処理はバックグラウンドで進み、フックは数ミリ秒でexit 0を返します。
# Heavy operation in background:
npx eslint --fix "$FILE" &
exit 0
落とし穴3 フックからプロジェクトの環境変数が見えない
フック内のprocess.envはNode.jsアプリと同じように使えると思っていましたが、違いました。フックはClaude Codeの親プロセスが起動するシェルで実行され、~/.zshrcまたは~/.bashrcにあるものと、Claude Codeが明示的に渡すものだけを参照します。フックが.envのOPENAI_API_KEYに依存するなら、.envを手動で読み込まなければなりません。
修正: フックコマンドで.envを明示的に読み込みます。
# Passing project env into hook:
cd "$PWD" && source .env && curl -H "Authorization: Bearer $OPENAI_API_KEY" ...
# Or with dotenv:
dotenv -e .env -- node my-validator.js
これは、それぞれのパッケージに独自の.envファイルがあるモノレポでは特に面倒です。PWDを渡し、コマンドの冒頭でcd "$PWD"を明示的に実行して解決できます。
コミット前にフックをテストするには?
重要なポイント
フックはシェルコマンドです。Claude Codeが実行する前に、偽のstdinを与えてターミナルで手動実行し、終了コードとstderrを確認できます。これにより、デバッグに何時間も費やさずに済み、自分自身の操作をブロックするフックからも守られます。
基本的なテストシナリオ。 JSONからフックコマンドを取り出し、ターミナルを開いてClaude Codeが送るJSONを渡します。
# Test hook 1 (protect .env):
echo '{"tool_input":{"file_path":".env"}}' | bash -c '
FILE=$(jq -r ".tool_input.file_path // empty")
[ -z "$FILE" ] && exit 0
echo "$FILE" | grep -qE "(^|/)\.env(\.|$)" && {
echo "BLOCKED" >&2
exit 2
} || exit 0
'
echo "Exit code: $?"
stderrにBLOCKED、Exit code: 2と表示されるはずです。exit 0になるなら、正規表現が機能していません。grepを修正してください。
確認するシナリオ:
- 成功パス - ファイルがブロックされること。フック1では
.env、フック3ではgit push --force main。 - 対象外パス - ファイルがブロックされないこと。フック1では
README.md、フック3ではgit push origin feature-branch。 - 境界ケース - 空の入力、null値、想定外の文字。フックはexit 0を返すか、明確なエラーで失敗するべきで、何も通知せずに通過してはいけません。
実験用フックの配置先。 .claude/settings.jsonではなく、.claude/settings.local.json(Git対象外)です。実際の操作の代わりにechoを使い、新しいフックをPostToolUseの下に追加します。発火することを確認してから、本番のsettings.jsonに移します。
// Experiment in settings.local.json - writes to /tmp/hook-debug.log:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "echo \"[hook fired] $(date) $(jq -r '.tool_input.file_path // \"?\"')\" >> /tmp/hook-debug.log"
}
]
}
]
}
}
Claude Codeで5〜10回編集した後、/tmp/hook-debug.logを開き、どのファイルでフックが発火したか確認します。空ならマッチャーが合っていません。正規表現を修正してください。
次は何をする?
重要なポイント
まずはexit 0を返すPostToolUseフックから始めましょう。最も安全で、何もブロックしません。1週間後、.envを保護したりforce-pushを防いだりする、exit 2の最初のPreToolUseフックを追加します。2週間後には、CLAUDE.mdは200行の契約書から、50〜80行の短いコンテキスト説明に縮まり、選んだイベント検査をフックへ移し、強いアクセス制限は権限、隔離、リモートの保護で別に強制します。
これからの1週間で行う5つのステップ:
CLAUDE.mdをgrepする。「never」「forbidden」「always」「must」などの語を探します。それぞれのルールはフックにできる候補です。命令形のルールは、テキストファイルでは強制できません。- PostToolUse + exit 0から始める。 最も安全です。変更は加えますが、何もブロックしません。prettier(フック2)、開発サーバーの終了(フック5)、テスト後の自動コミット(フック6)。
- 自信がついたら、最初のPreToolUse + exit 2を追加する。 たとえば
.envを保護する(フック1)、またはforce-pushを防ぐ(フック3)方法です。これはハードブロックなので、grepコマンドを間違えると自分自身もブロックされます。 - コミット前にフックを手動でテストする(上のセクションを参照)。デバッグの時間を大幅に節約できます。
- 実験には
settings.local.jsonを使う。 実際の操作の代わりにechoを使って、新しいフックを配置します。
フックを設定したら、このクラスターの次のステップはClaude Codeで変更を元に戻す方法です。フックが危険な操作を通してしまった場合に役立ちます。セッションをまたいでClaudeにプロジェクト構成を覚えさせたいなら、Claude Codeに第二の脳を作る(business/フォルダー+リンクされたナレッジベース)を構築してください。プロジェクトのスキルに名前を付けて再利用可能にする方法は、Claude Code Skillsをご覧ください。
出典
Ailootを通じてニューラルネットワークを利用したい場合は、Ailootをご覧ください。
詳細を見る
Claude Codeのフック2026:段階的な設定方法とすぐ使える7つの例:よくある質問
フックはなぜ入力JSONを1回だけ読む必要がありますか?
stdinはストリームで、最初のjqが消費すると後はEOFになります。一度保存してから項目を取り出します。結果がない・不明なら未検証として自動コミットを飛ばしてください。
例の正規表現フックは安全の境界になりますか?
いいえ。特定イベントと文字列の検査です。別表記、リダイレクト、スクリプト、MCP、他の書き込み経路は抜けえます。強制境界は認証、ファイル権限、隔離、保護ブランチで設けます。