Claude Code の hooks で自動チェックを仕込む
概要
Claude Code には、エージェントの動作の節目でユーザー定義のコマンドを実行する hooks という仕組みがあります。「ファイルを編集したら自動で整形する」「危険なコマンドを実行前にブロックする」といった処理を、プロンプトでのお願いではなく決定論的なフックとして差し込めます。
プロンプトで「編集後は必ず prettier をかけて」と頼んでも、守られたり守られなかったりします。hooks は AI の判断に依存せず、指定イベントで必ずコマンドが走るのが本質的な違いです。本記事では、次の3つをハンズオンで作ります。
PostToolUseで編集後に自動フォーマットPreToolUseで危険コマンドをブロックStopで作業完了時にチェックを走らせる
環境
- Claude Code(CLI)
- macOS / Linux(Windows は WSL 推奨)
jq(フックに渡る JSON を扱うため)
hooks の設定は .claude/settings.json に書きます。配置場所でスコープが変わります。
| ファイル | スコープ |
|---|---|
~/.claude/settings.json |
ユーザー全体 |
<repo>/.claude/settings.json |
プロジェクト(チーム共有・コミット対象) |
<repo>/.claude/settings.local.json |
プロジェクト(個人・gitignore 対象) |
hooks の基本構造
イベントごとに、matcher(対象ツール名の正規表現)と実行コマンドの組を登録します。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "echo 'ファイルが変更されました'" }
]
}
]
}
}
主なイベント:
| イベント | 発火タイミング |
|---|---|
PreToolUse |
ツール実行の直前(ブロック可能) |
PostToolUse |
ツール実行の直後 |
UserPromptSubmit |
ユーザーがプロンプトを送信した時 |
Stop |
エージェントが応答を終える時 |
SessionStart |
セッション開始時 |
matcher は Bash / Edit / Write などのツール名にマッチし、Edit|Write のように正規表現で複数指定できます。
フックに渡ってくる入力
フックのコマンドには、標準入力(stdin)経由で JSON が渡されます。PreToolUse / PostToolUse ではおおむね次の形です。
{
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf build/"
}
}
jq で必要なフィールドを取り出して処理します。
ハンズオン1: 編集後に自動フォーマット(PostToolUse)
Edit / Write が走ったら、対象ファイルに整形をかけます。tool_input.file_path に編集対象のパスが入っています。スクリプトに切り出すと読みやすいです。
.claude/hooks/format.sh:
#!/usr/bin/env bash set -euo pipefail file_path=$(jq -r '.tool_input.file_path // empty') [ -z "$file_path" ] && exit 0 [ -f "$file_path" ] || exit 0 case "$file_path" in *.ts|*.tsx|*.js|*.jsx) npx prettier --write "$file_path" ;; *.py) ruff format "$file_path" ;; *.go) gofmt -w "$file_path" ;; esac exit 0
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/format.sh" }
]
}
]
}
}
これで、AI が編集するたびに整形が走り、差分がフォーマッタ準拠に揃います。
ハンズオン2: 危険コマンドをブロック(PreToolUse)
PreToolUse は実行前に発火し、終了コードでツール実行を止められます。
- 終了コード 0: 許可
- 終了コード 2: ブロック(stderr の内容が Claude にフィードバックされる)
- それ以外: 非ブロックのエラー扱い
.claude/hooks/guard.sh:
#!/usr/bin/env bash set -euo pipefail command=$(jq -r '.tool_input.command // empty') if echo "$command" | grep -qiE 'rm[[:space:]]+-rf[[:space:]]+/|git[[:space:]]+push[[:space:]]+--force'; then echo "危険なコマンドを検出したためブロックしました: $command" >&2 exit 2 fi exit 0
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/guard.sh" }
]
}
]
}
}
stderr に書いたメッセージは Claude に返るため、単なる拒否ではなく理由つきのガードレールになります。より細かく制御するなら stdout に JSON を出す方法もあります。
echo '{"decision": "block", "reason": "破壊的コマンドは手動確認が必要です"}'
exit 0
ハンズオン3: 完了時にチェックを走らせる(Stop)
エージェントが応答を終える Stop イベントで、軽いチェックを回します。
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "npm run -s lint || true" }
]
}
]
}
}
Stop はツールに紐づかないため matcher は不要です。重い処理は体験を損なうので、lint や tsc --noEmit など軽いものから始めます。
確認方法
.claude/settings.jsonに hooks を書く。- Claude Code を起動し直す(設定の再読込のため)。
- 動作確認: 編集→整形、
rm -rfを含むコマンド→ブロック、応答終了→lint、をそれぞれ確認。 - フック単体はサンプル JSON を食わせて検証できます。
echo '{"tool_input":{"command":"rm -rf /"}}' | bash .claude/hooks/guard.sh
echo "exit=$?" # => exit=2 になれば成功
注意点
- hooks はあなたの権限でシェルを実行する。信頼できないリポジトリの設定を鵜呑みにしない。
- コマンドがハングするとセッションが待たされる。
|| trueやタイムアウトで早く返す。 PostToolUseのフォーマッタがファイルを書き換える点に注意。フック内からエージェントのツールを呼ばない。- パスはリポジトリルート起点で書く。
- イベント名や入力 JSON のフィールドはバージョンで変わりうるため、導入時に公式ドキュメントで最新を確認する。
まとめ
hooks は、AI の「お願いベース」の約束を決定論的な自動処理に変える仕組みです。PostToolUse で整形、PreToolUse でガードレール、Stop で完了時チェック——この3点セットが実用的です。制御は終了コード(2 でブロック)か stdout の JSON。実体はシェル実行なので、セキュリティレビューと軽量化を忘れずに。守ってほしいルールほど、お願いではなく仕組みに落とす価値があります。
