Claude Code hooks を使うと、Claude がツールを使う直前に自分のスクリプトを必ず走らせ、rm -rf や git push --force、.env の読み取りを実行前に止められます。やることは 2 つで、.claude/settings.json に PreToolUse の hook を書き、スクリプトから exit 2 か permissionDecision: "deny" を返すだけです。この記事では、筆者が実際に動かしたスクリプトと settings.json、echo '{...}' | ./hook.sh で試す方法、終了コードの意味、権限ルールとの使い分けまでを 2026 年 10 月時点の公式ドキュメントに沿って説明します。
hooks は、Claude Code のライフサイクルの決まった場面で、シェルコマンドや HTTP リクエストなどを自動で実行する仕組みです1。CLAUDE.md に「rm -rf は使わないで」と書いても、それは Claude への「お願い」にとどまります。hooks は Claude Code 本体が実行するので、モデルの判断に関係なく毎回走ります。
Claude が Bash を呼ぶと matcher に一致した hook スクリプトが stdin で JSON を受け取り、deny を返すとコマンドが実行されずに理由が Claude に戻る流れ図: 筆者作成(Claude Code 公式ドキュメントの hooks リファレンスをもとに作図)
設定は 3 段の入れ子です1。
- hook イベント: いつ動かすか(例:
PreToolUse はツール実行の直前)
- matcher: どのツールのときに動かすか(例:
"Bash"、"Edit|Write")
- hook ハンドラー: 何を動かすか(
command・http・mcp_tool・prompt・agent の 5 種類)
2026 年 10 月時点のリファレンスには 30 を超えるイベントがあります1。危険な操作を止める用途で押さえておくのは次の 6 つです。
| イベント | いつ動くか | 止められるか(exit 2) |
|---|
PreToolUse | ツールを実行する直前 | 止められる。ツール呼び出しを中止する |
PermissionRequest | 権限の確認ダイアログを出すとき | exit 2 は効かない。JSON の decision で拒否する |
PostToolUse | ツールが成功した直後 | 止められない(実行済み)。stderr を Claude に見せる |
UserPromptSubmit | プロンプトを送った直後 | 止められる。プロンプトを Claude に渡さない |
Stop | Claude が応答を終えるとき | 止められる。Claude に作業を続けさせる |
SessionStart | セッション開始・再開時 | 止められない。stdout を文脈として Claude に渡す |
危険なコマンドを「実行させない」なら PreToolUse 一択です。PostToolUse は実行したあとなので、消えたファイルは戻りません。
Claude Code は自分でシェルコマンドを実行するエージェントです。auto mode のように確認を自動で済ませる使い方が増えるほど、「人が毎回見る」前提が崩れます。hooks は、人が見ていないときにも効く最後の確認になります。
2026 年 10 月には、hooks を土台にした仕組みが 2 つ広がりました。
- プラグイン: hooks をスキルやサブエージェントと一緒に配れる。
hooks/hooks.json は settings.json の hooks と同じ書式です2
- Claude Mods: 2026-10-01 リリースの v2.1.287 で追加された、TypeScript の関数で振る舞いを変える仕組み34。公式は settings ファイルに書く従来の hooks を「settings hook」と呼んで区別しています
どちらも、まず settings hook の書式と終了コードの意味を知っていると理解が早くなります。Mods は Claude Code の Claude Mods 入門、配り方は プラグインとマーケットプレイス で扱います。
hooks は settings ファイルの hooks キーに書きます。置く場所で効く範囲が変わります1。
| 置き場所 | 効く範囲 | リポジトリで共有 |
|---|
~/.claude/settings.json | 自分の全プロジェクト | しない |
.claude/settings.json | そのプロジェクト | する(commit する) |
.claude/settings.local.json | そのプロジェクト(自分だけ) | しない |
| 管理ポリシー(managed settings) | 組織全体 | 管理者が配る |
プラグインの hooks/hooks.json | プラグインが有効な間 | プラグインで配る |
チームで同じガードを効かせたいなら、.claude/settings.json に書いて commit します。この記事の例は次の形です。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous.sh"
}
]
},
{
"matcher": "Read|Edit|Write|Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-env.sh"
}
]
}
]
}
}
書くときの注意は 3 つです。
- matcher の書き方: 英数字・
_・-・|・, だけなら完全一致(Edit|Write は Edit か Write)。それ以外の文字を含むと JavaScript の正規表現として扱われ、部分一致になります1
- パスは
$CLAUDE_PROJECT_DIR から書く: hook は現在のディレクトリで実行されるので、相対パスだと cd したあとに見つからなくなります。公式ガイドも "$CLAUDE_PROJECT_DIR" を使う例です5
- settings は厳密な JSON:
// のコメントや末尾のカンマは構文エラーになります6
command hook は、stdin で JSON を受け取り、終了コードと stdout で結果を返します1。ここを取り違えると「止めたつもりで止まっていない」ことになります。
| 返し方 | 意味 | PreToolUse での結果 |
|---|
exit 0(出力なし) | 問題なし | 通常の権限確認へ進む |
exit 0 + JSON(permissionDecision) | JSON で判断を伝える | deny なら中止、ask なら確認、allow なら確認を省く |
exit 2 | ブロック | 中止。stderr の文が理由として Claude に渡る |
exit 1 などそれ以外 | ブロックしないエラー | そのまま実行される |
いちばん大事なのは最後の行です。公式リファレンスは、ポリシーを強制する hook では exit 2 を使うよう警告しています。Unix で一般的な exit 1 は「ブロックしないエラー」扱いで、ツールはそのまま実行されます1。
PreToolUse の JSON は hookSpecificOutput の中に入れます1。
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "git push --force は禁止です。新しいブランチに push して PR を作ってください。"
}
}
permissionDecision は allow / deny / ask / defer の 4 つ。複数の hook が違う判断を返したら deny > defer > ask > allow の順で強い方が勝ちます
deny の理由は Claude に渡ります。「何をすればよいか」まで書くと、Claude が別の手を選びやすくなります
updatedInput を返すと、ツールの引数を書き換えられます(例: コマンドにオプションを足す)
hook には種類ごとに既定のタイムアウトがあります1。PreToolUse の command hook が時間切れになると、そのツール呼び出しはブロックされず通常の権限確認に進みます。重い処理を hook に入れないのが安全です。
hook ハンドラーの種類ごとの既定タイムアウト(秒、2026 年 10 月時点)データを表で見る
hook ハンドラーの種類ごとの既定タイムアウト(秒、2026 年 10 月時点)| 種類 | 既定のタイムアウト(秒)(秒) |
|---|
| command | 600 |
|---|
| http | 600 |
|---|
| mcp_tool | 600 |
|---|
| agent | 60 |
|---|
| prompt | 30 |
|---|
出典: Claude Code 公式ドキュメント Hooks reference の Common fields(2026-10-05 参照)。UserPromptSubmit などでは command・http・mcp_tool の既定値が 30 秒に下がる
ここからは、筆者が macOS(jq 1.8.1、Claude Code 2.1.289)で動かしたスクリプトです。.claude/hooks/block-dangerous.sh として保存し、chmod +x で実行権限を付けます。
#!/bin/bash
set -euo pipefail
input=$(cat)
cmd=$(jq -r '.tool_input.command // empty' <<<"$input")
deny() {
jq -n --arg reason "$1" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: $reason
}
}'
exit 0
}
if grep -Eq '(^|[;&|[:space:]])rm[[:space:]]' <<<"$cmd" \
&& grep -Eq '[[:space:]]-[a-zA-Z]*r|--recursive' <<<"$cmd" \
&& grep -Eq '[[:space:]]-[a-zA-Z]*f|--force' <<<"$cmd"; then
deny "rm -rf は hooks で止めています。消す対象をユーザーに確認してください。"
fi
if grep -Eq 'git[[:space:]]+push' <<<"$cmd" \
&& grep -Eq '[[:space:]](--force|--force-with-lease|-f)([[:space:]=]|$)|[[:space:]]\+[A-Za-z0-9_./-]+' <<<"$cmd"; then
deny "git push --force は禁止です。新しいブランチに push して PR を作ってください。"
fi
exit 0
ポイントは次のとおりです。
- stdin の JSON から
.tool_input.command を取り出す。Bash の tool_input には command・description・timeout・run_in_background が入ります1
- 止めるときは JSON で
deny を返し、exit 0 で終える。理由の文が Claude に届きます
--force-with-lease や git push origin +main のような書き方も止める。-f だけを見ると抜けます
.env は Read ツールでも、Bash の cat .env でも読めます。両方を見るために matcher を Read|Edit|Write|Bash にし、exit 2 で止めます。.claude/hooks/protect-env.sh です。
#!/bin/bash
input=$(cat)
tool=$(jq -r '.tool_name' <<<"$input")
if [[ "$tool" == "Bash" ]]; then
target=$(jq -r '.tool_input.command // empty' <<<"$input")
else
target=$(jq -r '.tool_input.file_path // empty' <<<"$input")
fi
if grep -Eq '(^|[/[:space:]])\.env(\.[A-Za-z0-9_-]+)?([[:space:]]|$)' <<<"$target" \
&& ! grep -Eq '\.env\.(example|sample|template)' <<<"$target"; then
echo "Blocked: .env には秘密情報が入っているので読めません。必要な変数名は .env.example を見てください。" >&2
exit 2
fi
exit 0
Read・Edit・Write の tool_input.file_path は、Claude Code が ~ や相対パスを展開した絶対パスで届きます1。そのため ../app/.env のような書き方で回り込まれる心配はありません。
hook は「入力が JSON、出力が終了コードと JSON」の普通のスクリプトです。Claude Code を起動しなくても、echo '{...}' | ./hook.sh で動きを確かめられます。
echo で PreToolUse の JSON を hook スクリプトに流し込み、rm -rf は deny、npm test は exit=0、.env の Read は exit=2 になることを確かめる様子筆者が実行した出力をそのまま録画(2026-10-05、macOS・jq 1.8.1)
筆者が試した入力と結果です。
入力(tool_input) | スクリプト | 結果 |
|---|
rm -rf ./dist | block-dangerous.sh | deny |
rm -r -f build | block-dangerous.sh | deny |
rm build.log | block-dangerous.sh | 通過(exit 0) |
git push origin main --force | block-dangerous.sh | deny |
git push -f origin feat / git push origin +main | block-dangerous.sh | deny |
git push origin feat / npm test | block-dangerous.sh | 通過 |
Read /work/app/.env・.env.local | protect-env.sh | exit 2 |
Read /work/app/.env.example・src/env.ts | protect-env.sh | 通過 |
Bash cat .env | protect-env.sh | exit 2 |
次に、実際のセッションでも確かめました。/tmp の作業用ディレクトリに上の settings.json を置き、claude -p で Claude に rm -rf build と .env の Read を頼むと、どちらも hook で止まり、build/ は消えずに残りました。.env のときは、Claude が「.env の読み取りがフックでブロックされた。.env.example を参照して」と hook の文を受けて返答しました。
対話セッションでは、/hooks メニューで読み込まれた hook を確認できます1。動かないときは claude --debug で起動し、~/.claude/debug/<session-id>.txt のログを見ます1。
hooks だけで守り切ろうとしないことが大切です。公式リファレンスも、Bash の if フィルターは「ベストエフォート」なので、厳密な許可・拒否には権限システムを使うよう書いています1。役割を分けると次のようになります。
| 仕組み | 得意なこと | 苦手なこと |
|---|
権限ルール(permissions.deny) | Read(./.env) のような決まったパスの拒否。コード不要 | 条件つきの判断(ブランチ名など) |
hooks(PreToolUse) | 正規表現や外部コマンドを使った柔軟な判定、理由の返却 | 文字列で判定するので、書き方を変えると抜けることがある |
| サンドボックス | OS レベルでファイル・ネットワークへのアクセスを遮断 | 設定の手間 |
.env については、権限ルールを先に入れておくのが確実です。公式の設定例は次のとおりです6。
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)"
]
}
}
Read の deny ルールは、組み込みのファイルツールに加え、Bash の cat・head・tail・sed・tee など Claude Code が認識するコマンドにも効きます。一方、Python や Node のスクリプトが内部でファイルを開く場合には効きません。すべてのプロセスを止めたいならサンドボックスを使います7。筆者は「権限ルールで決まったパスを拒否 → hooks で文脈つきの判定と理由の返却 → 本番の鍵は手元に置かない」の 3 段で考えています。
先に書いたとおり、exit 1 はブロックしません。止めたいときは exit 2 か、exit 0 + permissionDecision: "deny" にします1。
パスの書き間違いや実行権限の付け忘れでは、hook は「ブロックしないエラー」になり、ツールはそのまま実行されます。公式リファレンスは、ポリシー用の hook を入れたら最初の実行で hook error の表示が出ていないか確かめるよう勧めています1。chmod +x を忘れないでください。
{ で始まり } で終わる stdout だけが JSON として読まれます1。デバッグ用の echo は stderr(>&2)に出します。
cat ".env" のように引用符で囲む、変数を使う、スクリプト経由で読むなど、文字列の判定はすり抜けられます。hooks は「うっかり」を止める網と考え、権限ルールとサンドボックスを重ねます。
対話セッションでは、フォルダーの信頼ダイアログを承認するまで settings の hook は動きません。一方 -p や SDK ではダイアログが出ず、信頼済みとして扱われます。知らないリポジトリでは .claude/ を先に読むか、--settings '{"disableAllHooks": true}' で hook を止めて実行します1。
- Claude Code hooks は、決まった場面で Claude Code 本体が必ず実行するスクリプト。危険な操作は
PreToolUse で止める
- 止めるときは
exit 2 か、exit 0 + permissionDecision: "deny"。exit 1 は止まらない
echo '{...}' | ./hook.sh で、Claude Code を起動せずに動作を確かめられる
.env は権限ルールの Read(./.env) を先に入れ、hooks は文脈つきの判定と理由の返却に使う
- チームで使うなら
.claude/settings.json を commit するか、プラグインにして配る
次は、hooks をテスト駆動に組み込む Claude Code で Go のテスト駆動開発 や、AI が書いたコードを守る AI 生成コードのセキュリティ も参考にしてください。
自分だけなら ~/.claude/settings.json、チームで共有するならリポジトリの .claude/settings.json に書いて commit します。自分だけのプロジェクト設定は .claude/settings.local.json です。プラグインの hooks/hooks.json やスキル・サブエージェントの frontmatter にも書けます1。
どちらでもツールは止まります。理由の文を stderr に書くだけなら exit 2 が簡単です。ask で確認に回す、updatedInput で引数を書き換えるなど細かく制御したいときは、exit 0 で JSON を返します。なお exit 2 のブロックは JSON の allow でも上書きできません1。
まず echo '{...}' | ./hook.sh; echo $? でスクリプト単体の動きを見ます。次にセッションで /hooks を開き、読み込まれているか確かめます。それでも分からなければ claude --debug で起動し、デバッグログの matcher の一致や終了コードを見ます1。
できます。2026 年 10 月時点では、モデルの切り替え前に動く PreModelSwitch(切り替えを止められる)と、切り替え後に動く PostModelSwitch があります。matcher には claude-opus-5 のようなモデル名を書きます1。
settings hook はシェルコマンドや HTTP を外から実行し、許可・拒否・引数の書き換えなどを行います。Mods は Claude Code の中で動く JavaScript / TypeScript の関数で、画面に部品を描いたり、ツールの結果を書き換えたりもできます4。手持ちのスクリプトで止めるだけなら hooks で十分です。