Claude Code を使っていると、「編集したらフォーマッタを回して」「.env は触らないで」「実行したコマンドは記録しておいて」といった指示を毎回プロンプトに書きがちです。しかし、プロンプトでのお願いは Claude が従ってくれるかどうかが毎回の判断に依存します。
この問題は hooks で解決できます。hooks は、Claude Code のライフサイクルの決まったタイミングでシェルコマンドを実行する仕組みです。「お願い」ではなく「必ず実行される処理」として書けるのが利点です。
この記事では、手元で動かして確認した最小構成を次の順で紹介します。
- 設定ファイルの基本構造
- コマンド実行ログの記録
- 危険な編集のブロック
- 編集後の自動整形
なお、hooks の仕様は更新されることがあります。細かい挙動は公式ドキュメントも併せて確認してください。
hooks の基本: イベント・matcher・コマンド
hooks は「どのイベントで」「どのツールに対して」「何を実行するか」の3点で定義します。
よく使うイベントは次のとおりです。
| イベント | 実行タイミング |
|---|---|
PreToolUse |
ツール実行の直前(ブロックも可能) |
PostToolUse |
ツール実行の直後 |
UserPromptSubmit |
プロンプト送信時 |
Notification |
通知が出るとき(許可待ちなど) |
Stop |
Claude の応答が完了したとき |
ほかに SubagentStop、PreCompact、SessionStart、SessionEnd などがあります。
設定は次のいずれかの settings.json に書きます。
-
~/.claude/settings.json: ユーザー全体 -
.claude/settings.json: プロジェクト(チームで共有できる) -
.claude/settings.local.json: プロジェクト内の個人用(通常はコミットしない)
基本の構造は次の形です。
{
"hooks": {
"イベント名": [
{
"matcher": "ツール名のパターン",
"hooks": [
{ "type": "command", "command": "実行するコマンド" }
]
}
]
}
}
matcher には Bash や Edit|Write のようにツール名を指定します。大文字小文字は区別されます。
フック側には、イベントの情報が stdin に JSON で渡されます。session_id、cwd、tool_name、tool_input などが含まれるので、jq で必要な値を取り出します。以降の例では jq がインストールされている前提です。
1. まずは実行したコマンドを記録する
最初の一歩として、Claude が実行した Bash コマンドをファイルに追記するフックを作ります。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.command' >> ~/.claude/bash-command-log.txt"
}
]
}
]
}
}
これを ~/.claude/settings.json に書いて Claude Code を起動し、適当にコマンドを実行させると、ログが溜まっていきます。
tail -f ~/.claude/bash-command-log.txt
hooks の設定は /hooks コマンドでも確認できます。「JSON を書いたのに動かない」ときは、まずここで認識されているかを見ると切り分けが早いです。
2. 危険な編集をブロックする(exit 2)
PreToolUse の強みは、ツール実行を止められることです。終了コードの扱いは次のとおりです。
-
0: 成功。何もせず続行 -
2: ブロッキングエラー。stderr の内容が Claude にフィードバックされ、PreToolUseではツール呼び出しが止まる - それ以外: 非ブロッキングのエラー。処理は続行される
.env や secrets/ 配下への編集を止めるスクリプトを作ってみます。
#!/usr/bin/env bash
# .claude/hooks/protect.sh
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')
case "$file" in
*.env|*/secrets/*)
echo "保護対象のファイルのため編集できません: $file" >&2
exit 2
;;
esac
exit 0
実行権限を付けます。
chmod +x .claude/hooks/protect.sh
.claude/settings.json に登録します。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect.sh"
}
]
}
]
}
}
$CLAUDE_PROJECT_DIR はプロジェクトのルートを指す環境変数です。これを使えば、Claude が別のディレクトリに移動していてもスクリプトを見失いません。
Claude Code を起動する前に、スクリプト単体で動作確認しておくと安心です。
echo '{"tool_input":{"file_path":"/app/.env"}}' | .claude/hooks/protect.sh
echo $? # => 2 と保護メッセージが出ればOK
実際に Claude に .env の編集を頼むと、stderr のメッセージが Claude に渡り、別の方法を検討するなど、理由を踏まえた応答になりました。
3. 編集後に自動でフォーマットする
「編集のたびにフォーマッタを回して」と毎回頼む代わりに、PostToolUse に任せます。
#!/usr/bin/env bash
# .claude/hooks/format.sh
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
*.ts|*.tsx|*.js|*.json|*.md)
npx prettier --write "$file" >/dev/null 2>&1
;;
esac
exit 0
ここでは例として prettier を使っています。お使いのプロジェクトのフォーマッタに置き換えてください。登録は先ほどと同じ形です。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format.sh"
}
]
}
]
}
}
複数のフックは同じ hooks 配列に並べることもできます。PreToolUse の保護と PostToolUse の整形は、同じ settings.json にまとめて書けます。
4. ハマりどころと注意点
実際に試して引っかかりやすかった点をまとめます。
-
jqが無いと動かない: stdin の JSON を読む例はすべてjq前提です。事前に入れておきます。 -
matcher の書き間違い:
bashではなくBashです。大文字小文字を区別するので、動かないときはまずここを疑います。 -
設定変更が反映されない: 起動中のセッションでは、設定ファイルを直接編集しても即座には反映されないことがあります。その場合は
/hooksで確認するか、セッションを再起動します。 -
デバッグ:
claude --debugで起動すると、フックの実行状況を確認できます。 - 実行時間: フックにはタイムアウトがあります。重い処理を同期で回すと体感速度が落ちるので、軽い処理に留めるのがおすすめです。
-
セキュリティ: フックは自分のユーザー権限でそのまま実行されます。他人から受け取ったリポジトリの
.claude/settings.jsonに書かれたコマンドは、内容を確認してから使いましょう。入力値をシェルに展開する場合は、必ずクォートします。
まとめ
- hooks は「イベント × matcher × コマンド」で定義し、
settings.jsonに書く - 入力は stdin の JSON で渡されるので、
jqで取り出す -
PreToolUseでexit 2を返せば、ツール実行をブロックして理由を Claude に伝えられる -
PostToolUseに整形やチェックを置けば、毎回の指示が不要になる - 動かないときは
/hooksとclaude --debugで切り分ける
まずは「コマンドログの記録」のような副作用の小さいフックから始めて、慣れてきたら保護や自動整形に広げていくのが安全です。プロンプトに書いていた「毎回のお願い」を一つずつフックに移していくと、指示を書く手間がかなり減ります。