
最近、「Claude Code Hooks」が公開されて、話題になっていますね!どうやら、「Claude Code Hooks」を使うと、AIエージェントがrm -rfやgit push -fのような危険コマンドを実行する前に強制的にブロックできるようです!
そこで今回は、実際のプロジェクト環境で「Claude Code Hooks」による危険コマンドブロックの実装を行ってみました!settings.jsonの設定さえコピーすれば再現できる内容ですので、ぜひ皆さんも記事を読んで試してみてください!
この記事で分かること
- Claude Code Hooksの仕組みとPreToolUse/PostToolUseの違い
- CLAUDE.mdだけでは危険コマンドをブロックできない技術的理由
- settings.json全文と、rm -rf・git push -f・本番環境書き込みを防ぐ3つの実践テンプレート
Claude Code Hooksとは何か(PreToolUse/PostToolUseの違い)
結論から言うと、Claude Code Hooksとは、Claude Codeがツール(Bash、Edit、Writeなど)を実行する前後の特定のタイミングで、ユーザーが定義したシェルコマンドを必ず自動実行させる仕組みである。LLMの「気まぐれな判断」に依存せず、設定したコマンドが決定的に実行される点が最大の特徴となる。
Claude Code Hooksでできること
- 危険なシェルコマンドの実行前ブロック(PreToolUse)
- ファイル編集後の自動フォーマット・ログ記録(PostToolUse)
- 終了コードやJSON出力による細かい許可・拒否・確認の制御
ここで、PreToolUseとPostToolUseは何が違うのかという疑問が出てくると思う。
大きな差としては、「ツール実行前に割り込んで止められるかどうか」が可能ということだ。これにより、rm -rfのような破壊的コマンドが実行される前に強制停止することができる。
今話題の、「ガードレール型Hooks」というやつですね!
比較すると、以下のようになる。
| 項目 | PreToolUse | PostToolUse |
|---|---|---|
| 発火タイミング | ツール呼び出し実行前 | ツール呼び出し成功後 |
| 危険コマンドブロックの可否 | 可能(exit 2やpermissionDecision:denyで停止) | 不可能(既に実行済み) |
| 主な用途 | rm -rf・git push -f・sudo系のブロック、確認プロンプト化 | 自動フォーマット、ログ記録、テスト自動実行 |
| Claude Codeへの影響 | ツール呼び出し自体をキャンセルできる | 結果への追加処理のみで実行自体は止められない |
「settings.jsonの設定って難しそう…」と感じるかもしれませんが、この記事を読めば初心者の方でも大丈夫!順を追って危険コマンドブロックの完全な実装まで解説する。
危険コマンドがブロックされずに実行されてしまう仕組み
CLAUDE.mdだけでは防げない理由
結論として、CLAUDE.mdに「rm -rfは実行しないでください」と書くだけでは危険コマンドは防げない。CLAUDE.mdはあくまでLLMへの自然言語の指示であり、モデルの推論結果として「今回は例外的に実行してよい」と誤判断すれば、そのまま危険コマンドがシェルに渡ってしまうためだ。
Claude CodeはCLAUDE.mdの内容をコンテキストとしてプロンプトに含めるだけであり、実行エンジン側にハードな制約を課すものではない。つまりCLAUDE.mdは「お願い」であって「強制」ではなく、危険コマンドブロックの仕組みとしては信頼性が低い。
LLMの推論とシェル実行が分離している構造
Claude Codeの内部処理は、LLMが「次にBashツールでこのコマンドを実行する」と決定するフェーズと、実際にOSのシェルでコマンドを実行するフェーズに分かれている。この2つのフェーズの間に割り込めるのがPreToolUse Hooksであり、Hooksを設定していない場合、LLMが生成したコマンド文字列はノーチェックでシェルに渡され、そのまま実行されてしまう。
したがって、危険コマンドブロックを確実に行うには、LLMの判断結果を検証する「決定的なフィルター」をシェル実行の直前に設置する必要があり、それこそがClaude Code Hooksの本質的な役割となる。
settings.jsonの実装手順(危険コマンドブロック設定)
プロジェクト単位とグローバル設定の違い
settings.jsonにはスコープの異なる複数の配置場所がある。プロジェクト全体で危険コマンドブロックを共有したい場合は.claude/settings.json(Git管理下に置きチームで共有可能)、自分の全プロジェクトで常に有効にしたい場合は~/.claude/settings.json(マシンローカル)を使う。個人用の一時設定は.claude/settings.local.jsonに書き、これはgitignore対象となる。
| 配置場所 | スコープ | Git共有 |
|---|---|---|
| ~/.claude/settings.json | すべてのプロジェクト(グローバル) | 不可 |
| .claude/settings.json | 単一プロジェクト | 可能(推奨) |
| .claude/settings.local.json | 単一プロジェクト(個人用) | 不可(自動でgitignore) |
settings.json全文サンプル
以下は、危険コマンドブロックを含むsettings.jsonの完全な設定ファイルである。PreToolUseでBashコマンドを検査し、PostToolUseで編集内容をログに残す構成にしている。この1ファイルをそのまま.claude/settings.jsonに配置すれば動作する。
# ファイル名: .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-dangerous.sh",
"args": [],
"timeout": 10
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/log-bash.sh",
"args": [],
"timeout": 10
}
]
}
]
}
}
設定反映の確認方法
settings.jsonを直接編集した場合、変更はセッション再起動または/hooksコマンドで確認するまで反映されない。以下のコマンドで設定内容と反映状況を確認する。
# ファイル名: check-hooks.sh
# Claude Codeのターミナル内で実行
/hooks
# 設定ファイルの内容を直接確認
cat .claude/settings.json | jq .
危険コマンドブロックの実践テンプレート3種
テンプレート1:rm -rf系コマンドの正規表現ブロック
結論として、rm -rf系の危険コマンドは、Bashのtool_input.commandを正規表現で検査し、マッチした場合はexit 2でツール実行を強制停止する。以下がそのシェルスクリプト全文である。
#!/bin/bash
# ファイル名: .claude/hooks/block-dangerous.sh
# 危険コマンドブロック: rm -rf 系、sudo系を検知してPreToolUseで停止する
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# rm -rf / rm -fr / rm --recursive --force のいずれかにマッチ
DANGEROUS_PATTERN='rm[[:space:]]+(-[a-zA-Z]*[rf][a-zA-Z]*[rf]?[a-zA-Z]*|--recursive|--force)'
SUDO_PATTERN='^sudo[[:space:]]'
if echo "$COMMAND" | grep -Eq "$DANGEROUS_PATTERN"; then
echo "危険コマンドブロック: 破壊的な rm コマンドが検出されました -> $COMMAND" >&2
exit 2
fi
if echo "$COMMAND" | grep -Eq "$SUDO_PATTERN"; then
echo "危険コマンドブロック: sudo系コマンドは許可されていません -> $COMMAND" >&2
exit 2
fi
exit 0
このスクリプトの動作原理は、stdinから受け取ったJSONをjqで解析し、tool_input.commandフィールドを抽出したうえで正規表現マッチングを行う点にある。マッチした場合はexit 2を返す。Claude Code HooksではPreToolUseに限り終了コード2が特別扱いされ、stderrの内容がClaudeにエラーとしてそのまま伝えられ、ツール呼び出し自体がキャンセルされる。マッチしなければexit 0で通常の権限フローに戻し、他の安全なコマンドの実行を妨げない。
実際に動かした際のターミナル出力は以下のようになる。
$ claude
> プロジェクト内の不要なビルド成果物を削除して
● Bash(rm -rf ./dist)
⎿ Error: 危険コマンドブロック: 破壊的な rm コマンドが検出されました -> rm -rf ./dist
⎿ Blocked by hook (PreToolUse)
Claude: 危険コマンドブロックの仕組みによりrm -rfの実行が拒否されました。
代わりに対象ファイルを個別指定して削除する方法を提案します。
テンプレート2:git push -fのブロックと確認プロンプト化
git push -fは即座に拒否するのではなく、ユーザーに確認を求める形にする方が実用的である。以下は、JSON出力のpermissionDecision: "ask"を使って確認プロンプト化する実装である。
#!/usr/bin/env python3
# ファイル名: .claude/hooks/confirm-force-push.py
# git push -f / --force / --force-with-lease を検知して確認プロンプトに変換する
import json
import re
import sys
data = json.load(sys.stdin)
command = data.get("tool_input", {}).get("command", "")
FORCE_PUSH_PATTERN = re.compile(r"git\s+push\s+.*(-f\b|--force\b|--force-with-lease\b)")
if FORCE_PUSH_PATTERN.search(command):
result = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": (
"危険コマンドブロック対象: git push -f が検出されました。"
"リモートの履歴を上書きするため、実行前に必ず内容を確認してください。"
)
}
}
print(json.dumps(result, ensure_ascii=False))
sys.exit(0)
sys.exit(0)
このコードはstdinのJSONからtool_input.commandを取り出し、正規表現でgit push -f系のパターンを検出する。マッチした場合、exit 2で即ブロックするのではなく、stdoutにpermissionDecision: "ask"を含むJSONを出力する点がテンプレート1との違いだ。Claude CodeはこのJSONを受け取ると、通常の許可ダイアログを表示し、ユーザーが明示的に承認しない限りgit push -fを実行しない。settings.json側では、このスクリプトをPreToolUseのBashマッチャーに登録するだけでよい。
# ファイル名: .claude/settings.json(抜粋)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PROJECT_DIR}/.claude/hooks/confirm-force-push.py",
"args": []
}
]
}
]
}
}
実際に動かした際のターミナル出力は以下のようになる。
$ claude
> mainブランチにforce pushして
● Bash(git push -f origin main)
⎿ Permission required
⎿ 危険コマンドブロック対象: git push -f が検出されました。
リモートの履歴を上書きするため、実行前に必ず内容を確認してください。
この操作を許可しますか? (y/n):
テンプレート3:本番環境ディレクトリへの書き込み拒否
本番環境ディレクトリ(例:/production)や.envのような機密ファイルへの書き込みは、Edit/Writeツールのtool_input.file_pathをチェックして無条件にブロックするのが安全である。
#!/usr/bin/env python3
# ファイル名: .claude/hooks/protect-production.py
# /production ディレクトリと .env 系ファイルへの書き込みを拒否する
import json
import sys
BLOCKED_PATTERNS = [
"/production",
".env",
".env.local",
".env.production",
"secrets/",
"credentials",
]
data = json.load(sys.stdin)
file_path = data.get("tool_input", {}).get("file_path", "")
for pattern in BLOCKED_PATTERNS:
if pattern in file_path:
print(
f"危険コマンドブロック: {file_path} は保護対象のため書き込みできません(settings.jsonのHooksにより拒否)",
file=sys.stderr,
)
sys.exit(2)
sys.exit(0)
このスクリプトはEditまたはWriteツールが呼ばれるたびにtool_input.file_pathを検査し、保護対象パターンのいずれかが含まれていればexit 2でブロックする。settings.json側ではmatcherをEdit|Writeに設定することで、両方のツールに対して危険コマンドブロックが機能する。
# ファイル名: .claude/settings.json(抜粋)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-production.py",
"args": []
}
]
}
]
}
}
実際に動かした際のターミナル出力は以下のようになる。
$ claude
> .envファイルにAPIキーを追記して
● Write(.env)
⎿ Error: 危険コマンドブロック: .env は保護対象のため書き込みできません(settings.jsonのHooksにより拒否)
⎿ Blocked by hook (PreToolUse)
Claude: .envへの直接書き込みはブロックされました。
代わりに.env.exampleへのサンプル追加、または環境変数管理ツールの利用を提案します。
よくある失敗と対処法
終了コードの意味を誤解するケース
最もよくある失敗は、PostToolUseでexit 2を使ってしまい、ブロックされると誤解するケースである。exit 2による危険コマンドブロックはPreToolUseでのみ有効であり、PostToolUseやStopなど他のイベントでexit 2を返しても、単なるエラーとして扱われるだけでツール実行はすでに完了している。危険コマンドブロックを目的とする場合は、必ずPreToolUseにhookを登録する必要がある。
| 終了コード | 意味 | 備考 |
|---|---|---|
| 0 | 正常終了 | stdoutは詳細モードでのみ表示 |
| 2 | ブロック | PreToolUseでのみ有効。stderrがClaudeに渡される |
| それ以外 | エラー扱い | 実行はブロックされず継続する |
Hooksが反応しない場合のデバッグ手順
settings.jsonを編集してもHooksが発火しない場合、以下の手順で原因を切り分ける。
# 手順1: 設定ファイルのJSON構文エラーを確認
cat .claude/settings.json | jq .
# エラーが出る場合はJSON構文が壊れている
# 手順2: /hooks コマンドで登録状況を確認(Claude Codeターミナル内)
/hooks
# 手順3: スクリプトを単体実行してロジック自体を検証
echo '{"tool_input":{"command":"rm -rf ./tmp"}}' | ./.claude/hooks/block-dangerous.sh
echo "exit code: $?"
# 手順4: 実行権限が付与されているか確認
chmod +x .claude/hooks/*.sh
# 手順5: Ctrl+R でトランスクリプトモードに切り替え、Hook実行ログを確認
特に見落としやすいのが手順4のスクリプト実行権限で、chmod +xを忘れるとHooksが無反応のまま危険コマンドが素通りしてしまう。また、settings.jsonを直接編集した場合はセッション再起動が必要になる点も、危険コマンドブロックが機能しないよくある原因である。
Subagentsと組み合わせた高度な運用
レビュー専任Subagentの設計
Claude Code HooksとSubagentsを組み合わせると、危険コマンドブロック後に「なぜ拒否されたか」「代わりに何をすべきか」を自動提案する運用が実現できる。まず、レビュー専任のSubagentをYAML frontmatter付きのMarkdownファイルとして定義する。
# ファイル名: .claude/agents/safety-reviewer.md
---
name: safety-reviewer
description: 危険コマンドブロック後に安全な代替案を提示する専門エージェント
tools:
- Read
- Grep
- Glob
model: inherit
---
あなたは安全なコマンド実行のレビュー専任エージェントです。
Hooksによってブロックされたコマンドの意図を読み取り、
以下の観点で代替案を提示してください。
1. 元のコマンドが達成しようとしていた目的の推測
2. settings.jsonのHooksでブロックされた理由の説明
3. 同じ目的を安全に達成できる代替コマンドの提案(rm -rfなら特定ファイル指定など)
Hooksブロック後にSubagentが代替案を提示するフロー
次に、PreToolUseのHooksでBashコマンドを検査し、危険コマンドと判定した場合はブロックと同時に、safety-reviewerを呼び出すよう誘導する設定をsettings.jsonに追加する。
# ファイル名: .claude/settings.json(Subagents連携部分)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-dangerous.sh",
"args": []
}
]
}
]
}
}
この構成では、block-dangerous.shがexit 2でコマンドをブロックすると、Claude本体はstderrのメッセージを受け取り、続くターンで自律的にsafety-reviewer Subagentへタスクを委譲する判断を行う。実際の流れは、(1)ユーザーがrm -rfを含む指示を出す、(2)PreToolUse Hooksが危険コマンドブロックを実行、(3)safety-reviewerがブロック理由と元のコマンドを解析、(4)安全な代替コマンドを提示、という4段階になる。settings.jsonそのものにSubagent呼び出しを直接書く必要はなく、Hooksが返すエラーメッセージの内容を具体的かつ文脈豊富にしておくことが、Claudeが自律的に適切なSubagentへ処理を渡す精度を左右する重要なポイントとなる。
この設計により、危険コマンドブロックは単なる「拒否」で終わらず、開発者が次に取るべき行動まで提示する実用的な安全機構として機能する。settings.jsonとSubagentsの役割分担を明確にしておくことで、チーム開発における危険コマンドブロックの運用コストを大きく下げられる。