この記事の結論
Claude Code hooks は万能な安全装置ではなく、AI の実行経路に人間が決めた停止条件を差し込む仕組みだ。非エンジニア向けに言えば、「送信前に個人情報を確認する」「公開前に根拠を確認する」のような必ず挟むチェックポイントを、AI作業にも用意する発想である。
この記事の読み方
第1〜4章と第7章だけで十分です。hooks は「AIの作業中に自動で入る確認ルール」と読み替えてください。第6章のコードは、仕組みを作る人へ渡す仕様書として眺めればOKです。
第5〜6章まで読むと、settings・MCP・秘密情報・終了時確認を実装レベルで設計できます。
この記事でわかること
- ✓Claude Code hooks は「AIを賢くする機能」ではなく、自動で入る確認・停止ポイントであること
- ✓permissions・hooks・sandbox・MCP承認の4レイヤーの役割分担
- ✓hooks が守れるものと、どう頑張っても守れないものの明確な線引き
- ✓ChatGPT・Claude・NotebookLM でも応用できる、AI作業の安全チェックリスト
- ✓なぜ hooks と設定ファイル自体が攻撃面になり得るのか(CVE事例付き)
- ✓今すぐ入れられる最小構成5選(サンプルコード付き)
前の記事との接続
前回の記事「プロンプトインジェクションを徹底的に調べた」で、6階層の防御戦略の中でも全体設計の層が最重要という結論を出した。今回はその実践編として、Claude Code の hooks を題材にする。ただし主題はコードではなく、AIに任せる前後で「どこに確認を挟むか」という運用設計だ。
hooks とは何者か
最初にここをはっきりさせる。Claude Code の hooks は「AI を賢くする機能」ではない。
正確に言うと、「AI が何かをしようとしたとき、その直前・直後・終了時に、人間が決めた境界線を差し込むための自動で入る割り込みポイント」だ。LLM が気分でチェックを実行するかどうかに依存せず、hooks は必ず発火する。これが通常のプロンプト指示との本質的な違いになる。
ふつうのAI利用に置き換えると
hooks は「AIに毎回お願いする注意書き」ではなく、「提出前チェックリスト」「送信前の個人情報確認」「公開前の根拠確認」に近い。ChatGPT や Claude の通常チャットでは自動化まではできなくても、会話の最後に必ず確認する項目を決めるだけで同じ考え方を使える。
⚠ ただし同時に「刃物」でもある
公式リファレンスは、command hooks がユーザーのシステム権限そのままでシェルコマンドを実行すると明記している。hooks は安全ベルトであると同時に、フルユーザー権限で動く実行エンジンだ。「hooks を入れれば守られる」ではなく、「hooks もまた設計の一部として慎重に扱う」が正しい前提になる。
したがって、防御設計として正しい言い方は「hooks で守る」ではなく、「permissions・sandbox・MCP制御・人間承認の上に hooks を重ねる」だ。この記事はその全体像を整理する。
防御レイヤーの役割分担
Claude Code の防御は大きく4つのレイヤーに分かれている。役割を混同すると「やった気」になって穴が残る。
技術用語を生活語にすると
何を許すか・許さないかの設定値。deny で特定のツール呼び出しをブロックする基線になる。ただし Claude の組み込みツールには効いても Bash 経由は止まらない——これが最大の盲点。
実行の前後で条件分岐・ブロック・監査ログを差し込む層。exit 2 でアクションをブロック、exit 0 で通過。この記事の主役。
OS レベルでファイル・ネットワーク境界を強制する層。Read(./.env) の deny は Bash の cat .env を止めないが、sandbox のファイル分離はこれを強制できる。hooks では代替できない唯一の強制手段。
外部ツールや未知のプロジェクト設定を飲み込む前の入口管理。.mcp.json には command・args・env・headers への環境変数展開があり、headersHelper は任意シェルコマンドを実行できる。hooks を固めても MCP が緩ければ意味がない。
💡 ポイント
4つのレイヤーは「どれか一つで十分」ではなく、それぞれが別の経路を塞いでいる。permissions で基線を引き、hooks で条件分岐を入れ、sandbox で OS 強制し、MCP を厳選する——この順番が崩れると必ず穴が残る。
hooks が効くライフサイクル全体
hooks はセッション開始から終了まで、Claude Code のライフサイクル全体に差し込める。逆に言うと、「どこで止めるべきか」を設計しないと、hooks を入れても肝心なタイミングを外す。
非エンジニアが持ち帰るなら
覚えるべきはイベント名ではなく、開始時・実行前・実行後・終了時の4か所だ。資料作成なら「開始時に目的確認」「実行前に素材確認」「実行後に根拠確認」「終了時に公開可否確認」と置き換えられる。
| イベント | 発火タイミング | できること |
|---|---|---|
| SessionStart | セッション開始時 | ブランチ・dirty files を文脈として注入 |
| UserPromptSubmit | ユーザープロンプト送信時 | 危険語検知・注意文脈を追加 |
| UserPromptExpansion | スラッシュコマンド展開時 | 特定コマンドの直実行をブロック |
| PreToolUse | ツール実行直前 | 最重要。ここでブロックするのが最も効果が高い |
| PermissionRequest | 許可ダイアログ直前 | 安全な定型のみ自動 allow・危険系は deny |
| PostToolUse | ツール実行直後 | 整形・監査ログ・diff summary(止められない) |
| Stop | セッション終了時 | 初回 stop をブロックして安全サマリーを要求 |
| ConfigChange | settings 変更時 | 設定変更の監査・ブロック |
| InstructionsLoaded | 指示ファイル読込時 | いつ何が読み込まれたか監査(ブロック不可) |
| FileChanged | 監視ファイル変更時 | .mcp.json 等の変更を監査 |
| CwdChanged | 作業ディレクトリ変更時 | 新しい環境の文脈を再注入 |
exit code の違いを知っておく
exit 2 はアクションをブロックして会話継続を要求する。exit 1 は多くのイベントで非ブロッキングエラー扱い(セッションを止めない)。exit 0 は通過。「止めたいのに止まらない」最多原因が exit 1 を使っていること。
守れるものと守れないもの
ここを弱く書くと危険な誤解が生まれる。hooks の守備範囲と限界を正直に分ける。
非エンジニア向けに一言で言えば、hooks は「うっかり実行」を減らす仕組みであって、AIが読んだ情報を完全に安全化する仕組みではない。怪しい資料を読ませる前の判断、個人情報を渡す前の判断、公開前の最終判断は人間側に残る。
hooks が効く場面
- ✓危険な Bash 実行の事前ブロック(PreToolUse + exit 2)
- ✓秘密ファイルへのアクセス拒否(Read/Edit/Write を止める)
- ✓設定変更の監査(ConfigChange + FileChanged)
- ✓終了前の安全確認(Stop)
- ✓実行後の整形・監査ログ(PostToolUse)
- ✓セッション開始時の安全文脈注入
hooks では守れない
- ✗モデルが既読の悪意ある指示を「忘れさせる」——読んだ後は手遅れ
- ✗外部コンテンツを完全に無害化する——文字列検査は迂回される
- ✗OS 権限そのものを縮小する——sandbox の仕事
- ✗実行済みの操作を巻き戻す——PostToolUse は事後監査であり undo ではない
- ✗Python・Node 等、別経路からの通信(bash deny では止まらない)
- ✗MCP サーバーの承認・接続そのもの——接続済み MCP ツールの呼び出しは hooks で監査できるが、入口管理は別レイヤー
⚠ 一番危険な思い込み
Read(./.env) を deny に入れて「秘密が守られた」と思う状態が最も危険だ。Claude の組み込み Read は止まっても、Bash の cat .env は止まらない。permissions と hooks の deny を組み合わせ、さらに必要なら sandbox を使う——この多層を省略するとここで必ず漏れる。
なぜ hooks 自体が攻撃面になるのか
hooks を入れることで守りが厚くなるのは正しい。しかし同時に、hooks の仕組みそのものが攻撃経路になり得ることを知らないと、かえって誤った安心感が生まれる。
非エンジニア向けの読み替え
「便利な自動化」ほど、誰が作ったものか・何にアクセスするのかを確認する必要がある。Chrome拡張、Notion連携、Google Drive連携、AIの外部ツール連携も同じで、便利さと権限はセットで増える。
command hooks はフルユーザー権限で動く
command hooks が実行するシェルスクリプトは、あなたのシステムユーザー権限そのまま実行される。悪意ある hook スクリプトが紛れ込めば、任意のコマンドをフル権限で実行できる。「hook 設定ファイルはただの JSON」ではなく、実行可能なエントリポイントとして扱う必要がある。
.claude/settings.json はリポジトリにコミットできる
project scope の .claude/settings.json はチーム共有前提でリポジトリに入る。悪意ある hooks が仕込まれたリポジトリを clone してセッションを起動すると、その hooks が即座に有効になる。「未知のリポジトリは trust verification を通す前にセッションを始めない」が原則になる。
.mcp.json もまた「実行ファイル」だ
.mcp.json は command・args・env・url・headers への環境変数展開を持ち、headersHelper フィールドは任意シェルコマンドを実行できる。従来の「設定ファイル」より実行可能性がずっと高い。また .mcp.json は ConfigChange の対象外のため、FileChanged での補完が必要になる。なお、接続済み MCP ツールの呼び出し自体は mcp__<server>__<tool> 形式で hooks の matcher 対象にできる。
実際に CVE が出ている
セキュリティ研究機関が .claude/settings.json・.mcp.json・ANTHROPIC_BASE_URL 等のプロジェクト設定を使った RCE と API キー流出の不具合を報告。GitHub Advisory Database と NVD に CVE として公開され、修正バージョンも明示されている。
特に重要な点は、startup trust dialog の前にコード実行できた問題だ。「設定ファイルを承認する前に実行が始まってしまう」という問題は、「設定ファイルは ただの設定」という前提が誤りであることを端的に示している。手動更新運用ならバージョン管理は防御設計の一部になる。
組織運用での追加の閉じ方
個人利用では重いが、チームでの運用では検討に値する設定がある。managed settings の allowManagedHooksOnly で user/project/plugin hooks を一括無効化できる。HTTP hooks の送信先を allowedHttpHookUrls で絞ることも可能だ。MCP も allowedMcpServers・allowManagedMcpServersOnly で管理できる。
設計者自身が「hooks は危険たり得る」と判断してガードを用意しているわけで、その意図を理解せずに使うと穴になる。
最初に入れる5つ——最小防御構成
いちばん実務的な勧め方は、「共有プロジェクト設定」から始めないことだ。最初の防御設定は ~/.claude/settings.json(user scope)か、少なくとも gitignore される .claude/settings.local.json(local scope)に置く。
project scope の .claude/settings.json は共有には便利だが、まさにそこが前章で触れた持ち込み攻撃の入口にもなる。user scope はそのユーザーの全セッションに効くので、個人防御として最も筋がいい。
コードを書かない人向け:5つの安全ルール
前提:jq のインストールとディレクトリ準備
brew install jq
mkdir -p .claude/hooks .claude/audit
command hook のサンプルは公式ガイドと同様に jq で stdin の JSON を解析する前提で書いている。
01 危険 Bash コマンドのブロック
防ぐもの:破壊的コマンド・パイプ経由インストール・権限昇格のうっかり実行
非エンジニア向けに言えば:削除・送信・公開のような戻しにくい操作を、AIに即実行させないルール。
最優先。破壊的削除・curl | bash・sudo を実行前に止める。prompt injection 対策にも事故防止にも効く。
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous-bash.sh"
}
]
}
]
#!/bin/zsh
set -eu
cmd="$(jq -r '.tool_input.command // ""' < /dev/stdin)"
dangerous_patterns=(
'(^|[[:space:]])sudo([[:space:]]|$)'
'rm[[:space:]]+-rf[[:space:]]+/'
'mkfs'
'shutdown|reboot'
'curl[^|]*\|[[:space:]]*(sh|bash|zsh)'
'wget[^|]*\|[[:space:]]*(sh|bash|zsh)'
)
for p in "${dangerous_patterns[@]}"; do
if [[ "$cmd" =~ $p ]]; then
echo "Blocked potentially destructive shell command: $cmd" >&2
exit 2
fi
done
exit 0
注意点
文字列ベースの検査なので Bash の変種や迂回は残る。Python・Node など別経路は止まらない。deny ルールと sandbox との組み合わせが必要。
02 .env と秘密鍵ファイルの保護
防ぐもの:組み込み Read/Edit/Write からの秘密ファイル参照・編集
非エンジニア向けに言えば:住所・家計・口座・未公開資料など、AIに渡す前に伏せる情報を決めるルール。
permissions の deny だけで満足しない。Claude の組み込み Read は止まっても Bash は止まらない。permissions で基線を敷きつつ hooks で Read/Edit/Write を止めるのが現実的な最小構成。
"permissions": {
"deny": [
"Read(./.env)", "Read(./.env.*)",
"Read(./secrets/**)",
"Read(./**/*.pem)", "Read(./**/*.key)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Edit|Write",
"hooks": [{ "type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-secret-files.sh" }]
}
]
}
#!/bin/zsh
set -eu
path="$(jq -r '.tool_input.file_path // .tool_input.path // ""' < /dev/stdin)"
[[ -z "$path" ]] && exit 0
case "$path" in
*/.env|*/.env.*|*.pem|*.key|*/secrets/*|*/id_rsa|*/id_ed25519)
echo "Blocked access to a secret-like file: $path" >&2
exit 2
;;
esac
exit 0
注意点
この構成だけでは cat .env のような Bash 経路は止まらない。「秘密保護したつもり」が最も危険な状態。
03 外部通信の許可リスト
防ぐもの:WebFetch の想定外ドメイン取得と、代表的な Bash 通信コマンド
非エンジニア向けに言えば:AIに読ませる外部情報を、公式サイト・信頼できる資料・自分で確認したURLに絞るルール。
外部送信は prompt injection の危険が本格化する条件として最重要。Bash の代表的な通信ツールを deny しつつ、WebFetch は allowlist で制御する。完全なネットワーク遮断は sandbox で行う。
"Bash(curl *)", "Bash(wget *)", "Bash(nc *)",
"Bash(ncat *)", "Bash(scp *)", "Bash(rsync *)", "Bash(ssh *)"
#!/bin/zsh
set -eu
input="$(cat)"
tool="$(jq -r '.tool_name // ""' <<<"$input")"
allow_domains=("code.claude.com" "docs.anthropic.com" "github.com")
if [[ "$tool" == "WebFetch" ]]; then
url="$(jq -r '.tool_input.url // ""' <<<"$input")"
for d in "${allow_domains[@]}"; do
if [[ "$url" == *"://$d"* || "$url" == *"://www.$d"* ]]; then
exit 0
fi
done
echo "WebFetch blocked: domain not in allowlist." >&2
exit 2
fi
exit 0
注意点
Python・Node・git 等の通信まで完全に制御したいなら sandbox のネットワーク allowlist が必要。Bash パターンで URL を縛るのは fragile で、変数経由やリダイレクトで崩れる。
04 .claude/settings.json と .mcp.json の変更監査
防ぐもの:設定変更の見落とし。ブロックではなく、後から追える状態を作る
非エンジニア向けに言えば:AI連携・共有範囲・公開設定を変えたときに、あとで見返せる記録を残すルール。
ConfigChange は settings の監査に使えるが、.mcp.json は対象外。settings は ConfigChange、.mcp.json は FileChanged で補うのが実務的。
"ConfigChange": [
{ "hooks": [{ "type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/audit-config.sh" }] }
],
"FileChanged": [
{
"matcher": ".mcp.json|settings.json|settings.local.json",
"hooks": [{ "type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/audit-config.sh" }]
}
]
#!/bin/zsh
set -eu
mkdir -p "$CLAUDE_PROJECT_DIR/.claude/audit"
input="$(cat)"
event="$(jq -r '.hook_event_name // ""' <<<"$input")"
timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
case "$event" in
ConfigChange)
source="$(jq -r '.source // ""' <<<"$input")"
path="$(jq -r '.file_path // ""' <<<"$input")"
printf '%s\t%s\t%s\t%s\n' "$timestamp" "$event" "$source" "$path" \
>> "$CLAUDE_PROJECT_DIR/.claude/audit/config-events.log"
;;
FileChanged)
path="$(jq -r '.file_path // ""' <<<"$input")"
change="$(jq -r '.event // ""' <<<"$input")"
printf '%s\t%s\t%s\t%s\n' "$timestamp" "$event" "$change" "$path" \
>> "$CLAUDE_PROJECT_DIR/.claude/audit/config-events.log"
;;
esac
exit 0
注意点
これは監査ログであり、ブロックではない。「Claude に設定を書き換えられたくない」なら別途 PreToolUse で .claude/settings.json への Edit/Write をデフォルト拒否にする。
05 作業終了時のセーフティサマリー
防ぐもの:変更内容・テスト状況・設定ファイル変更を確認しないまま終了すること
非エンジニア向けに言えば:投稿・資料・家計整理の最後に、変更点・根拠・未確認点を必ず要約させるルール。
非エンジニアにこそ効く。Stop hook は Claude が「終わった」と言う瞬間に割り込める。初回 stop をブロックして「本当に今終わっていいか」を一度だけ問い直す。stop_hook_active で無限ループを防げる。
#!/bin/zsh
set -eu
input="$(cat)"
active="$(jq -r '.stop_hook_active // false' <<<"$input")"
# 2回目の stop は通す(無限ループ防止)
[[ "$active" == "true" ]] && exit 0
changed="$(git -C "$CLAUDE_PROJECT_DIR" diff --name-only -- . || true)"
staged="$(git -C "$CLAUDE_PROJECT_DIR" diff --cached --name-only -- . || true)"
if [[ -n "$changed$staged" ]]; then
sensitive="$(printf '%s\n%s\n' "$changed" "$staged" | \
grep -E '(^|/)(\.claude/|\.mcp\.json|package\.json|\.env|.*\.pem)$' || true)"
echo "Before stopping, give a short safety summary: changed files, whether tests/lint were run, and highlight config or secret-sensitive files. Sensitive matches: ${sensitive:-none}" >&2
exit 2
fi
exit 0
注意点
テストを自動実行するわけでも、壊した変更を巻き戻すわけでもない。「うっかり終わるのを防ぐ最後の声かけ」として位置付ける。
5つをまとめた最小 settings.json
以下はそのまま完成品として配るための設定ではなく、環境に合わせて調整するたたき台だ。導入後は /doctor と claude --debug hooks で実際に発火することを確認する。
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"defaultMode": "default",
"deny": [
"Bash(curl *)", "Bash(wget *)", "Bash(nc *)",
"Bash(ncat *)", "Bash(scp *)", "Bash(rsync *)", "Bash(ssh *)",
"Read(./.env)", "Read(./.env.*)",
"Read(./secrets/**)",
"Read(./**/*.pem)", "Read(./**/*.key)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous-bash.sh" }]
},
{
"matcher": "Read|Edit|Write",
"hooks": [{ "type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-secret-files.sh" }]
},
{
"matcher": "WebFetch|Bash",
"hooks": [{ "type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/allow-network.sh" }]
}
],
"ConfigChange": [
{ "hooks": [{ "type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/audit-config.sh" }] }
],
"FileChanged": [
{
"matcher": ".mcp.json|settings.json|settings.local.json",
"hooks": [{ "type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/audit-config.sh" }]
}
],
"Stop": [
{ "hooks": [{ "type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/stop-safety-summary.sh" }] }
]
}
}
導入後の確認コマンド
claude
# セッション内で確認
/hooks # 読み込まれた hooks の一覧
/permissions # 実効権限
/mcp # MCP サーバー状態
/status # 設定状態の確認
/doctor # 設定競合・schema error のチェック
hook が発火しないときは claude --debug hooks が有効。
多層防御という構図に戻す
最後に全体の構図を整理する。比喩として使いやすい言い方がある。
安全ベルトは hooks、車体は permissions と sandbox、道路ルールは human approval
どれか一つでは守れない。全部揃って機能する。
hooks を入れると「守られた感」は強くなる。しかし本章で見てきたとおり、hooks が止められるのは実行前のブロックと事後の監査だけで、OS 権限の縮小や既読内容の無害化はできない。通常のAIチャットでも同じで、「AIに注意してもらう」だけでは足りない。素材を絞る、共有範囲を決める、公開前に人間が見る、という外側の設計が必要になる。
そして防御設計は「最初に完璧に作る」よりも、少しずつ育てる方が実践的だ。エンジニアなら user scope に5つのスクリプトを入れる。非エンジニアなら、まず終了前チェックと個人情報チェックを毎回の型にする。この繰り返しが、AI ツールを安全に使い続けるための現実的な道筋になる。
自分が今日から変えること
- →非エンジニア:AIに渡す前に、個人情報・未公開情報・共有範囲を1分だけ確認する
- →非エンジニア:会話の最後に「変更点・根拠・未確認点」を必ず要約させる
- →エンジニア:user scope に今回の5スクリプトを入れる(.claude/settings.local.json または ~/.claude/settings.json)
- →エンジニア:未知のリポジトリ、.mcp.json、Claude Code のバージョンを定期的に確認する
この記事を書くにあたっての前提
本記事は Anthropic 公式 docs を一次情報とし、公開済みのセキュリティ advisory と公開研究を重ねて整理したものだ。hooks の仕様・イベント名・exit code の挙動は公式 docs に基づいているが、設定例は「安全側に倒した最小サンプル」であり、あらゆる環境・用途を保証するものではない。
OWASP・UK NCSC とも、LLM を使うシステムに対して「完全防御」ではなく「リスクと影響の低減」で設計することを推奨している。hooks は有効だが銀の弾丸ではない。
主要参考文献
・ Anthropic Claude Code Docs: Hooks reference / Hooks guide / Settings / Permissions / Security / MCP / Sandboxing
・ Anthropic Claude Code Docs: Explore the .claude directory / Debug your configuration
・ GitHub Advisory Database: GHSA-4fgq-fpq9-mr3g / GHSA-jh7p-qr78-84p7(CVE-2025-59536・CVE-2026-21852)
・ Check Point Research: "Caught in the Hook: RCE and API Token Exfiltration Through Claude Code Project Files"
・ OWASP Cheat Sheet Series: AI Agent Security Cheat Sheet / LLM Prompt Injection Prevention Cheat Sheet
・ UK National Cyber Security Centre: "Prompt injection is not SQL injection"