Claude Code の PreToolUse フックで rm -rf を止める設定と、その限界

Claude Code 公開:

PreToolUse フックで危険な rm を拒否する書き方を、終了コード 2 と permissionDecision の 2 通りで解説します。bypassPermissions でも効く理由と、文字列一致では抜けられる限界も扱います。

検証日 2026年9月22日 仕様変更が早い分野です。最新の公式ドキュメントも併せてご確認ください。
目次
  1. 終了コード 2 で止める
  2. permissionDecision で理由を伝える
  3. ファイル編集側も止める
  4. 文字列一致の限界
  5. 動作を確認する
  6. まとめ

rm -rf のような取り返しのつかないコマンドだけは、確認を挟まずに実行されると困ります。PreToolUse フックを使うと、ツール呼び出しの直前にスクリプトで判定して拒否できます。

このフックの強みは、権限モードより先に発火することです。公式ドキュメントは、PreToolUse フックが dontAsk を含むすべての権限モードで権限チェックより先に発火し、permissionDecision: "deny" を返せば bypassPermissions--dangerously-skip-permissions でもツールをブロックすると明記しています。

KEY POINT

この記事で分かること

  • 終了コード 2 と permissionDecision の 2 通りの止め方
  • 権限モードを変えても迂回されない理由と、逆にできないこと
  • 文字列一致の限界と、併用すべき設定

終了コード 2 で止める

もっとも短い書き方です。スクリプトを stderr にメッセージを書いて exit 2 で終えるだけで、ツール呼び出しが拒否され、その stderr が Claude に渡されます。

#!/usr/bin/env bash
# .claude/hooks/block-rm-rf.sh
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

if echo "$CMD" | grep -Eq '(^|[;&|[:space:]])rm[[:space:]]+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)'; then
  echo "Blocked: 再帰的かつ強制の rm は禁止です。削除対象を個別に指定してください" >&2
  exit 2
fi

exit 0

macOS と Linux では実行権限が必要です。

chmod +x .claude/hooks/block-rm-rf.sh

登録は .claude/settings.json に書きます。公式ドキュメントの例と同じく、ログ用と判定用の 2 つを並べられます。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r .tool_input.command >> ~/.claude/bash.log"
          },
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh"
          }
        ]
      }
    ]
  }
}

公式ドキュメントはこの構成の動きをこう説明しています。2 つのフックは並列に実行され、ログ用は 0 で終了して判定を返さず、判定用が 2 で終了してツール呼び出しを拒否します。拒否が優先されるためコマンドはブロックされ、ログは先に実行済みなので記録も残ります。

permissionDecision で理由を伝える

終了コードの代わりに、stdout に JSON を出す方法もあります。理由を構造化して渡せます。

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "再帰削除は禁止です。git clean -n で確認してから個別に消してください"
  }
}

PreToolUse で指定できる値は 3 つです。

動作
allow対話的な権限プロンプトを省略する。deny ルールと ask ルールは引き続き適用される
denyツール呼び出しを取り消し、理由を Claude に渡す
ask通常どおり権限プロンプトを表示する

deny の場合、permissionDecisionReason がそのまま Claude にフィードバックされます。Claude はそれを読んで別の手段を取れるので、単に止めるより「なぜ駄目か・代わりに何をするか」を書いておくと作業が止まりません。

allow で deny ルールは上書きできない

逆方向は成立しません。公式ドキュメントは、フックが allow を返しても設定ファイルの deny ルールは迂回できず、requiresUserInteraction の MCP ツールや組織が ask にしたコネクタのプロンプトも抑制できないと説明しています。フックは制限を強められるが、権限ルールが許す範囲を超えて緩めることはできません

ファイル編集側も止める

rm は Bash ツール経由ですが、EditWrite で重要ファイルを壊されるのも同じ種類の事故です。公式ドキュメントはファイルパスを見て拒否する例を示しています。

#!/bin/bash
# protect-files.sh

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Windows のバックスラッシュ区切りを正規化して、下のパターンに一致させる
FILE_PATH="${FILE_PATH//\\//}"

PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")

for pattern in "${PROTECTED_PATTERNS[@]}"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
    exit 2
  fi
done

exit 0

この場合は matcherEdit|Write にして登録します。stdin の JSON の構造は hooks が受け取る stdin JSON の構造matcher に書ける値は hooks の matcher に書ける値一覧 を参照してください。

文字列一致の限界

ここが重要です。コマンド文字列の一致は簡単に外れます

書き方素朴な rm -rf 一致
rm -rf build一致する
rm -r -f build一致しない
rm --recursive --force build一致しない
FLAGS="-rf"; rm $FLAGS build一致しない
find . -deleteそもそも rm ではない

上のスクリプトは -rf-fr の並びに対応していますが、それでも網羅ではありません。フックは「うっかり」を止める運用上の歯止めとして有効で、悪意のある回避を防ぐ仕組みではありません

確実に遮断したい対象には、次の層を併用してください。

  • permissions の deny ルール: Bash(rm -rf *) のようなルールは、フックの結果に関わらず評価されます。公式ドキュメントは「フックの判断は権限ルールを迂回しない。PreToolUse フックが何を返しても deny ルールは評価される」と説明しています(settings.json で permissions を設計する)
  • サンドボックス: プロセス単位でファイルシステムへのアクセスを制限します。スクリプトが内部で開くファイルにも効きます

なお、critical path を対象にした rmrmdir は、どの権限モードでも自動承認されません。フックの allow でも承認されない、という点も公式に明記されています。

用語解説

PreToolUse: ツールが実行される直前に発火するイベントです。ここで拒否すると、ツール呼び出し自体が行われません。

動作を確認する

登録したら /hooks を実行して、対象のイベントの下にフックが出ているかを確認します。スクリプト単体の確認は、サンプル JSON を流し込むのが確実です。

echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/x"}}' | ./.claude/hooks/block-rm-rf.sh; echo "exit=$?"

exit=2 になれば期待どおりです。終了コードごとの意味は hooks の終了コードの意味と使い分け にまとめています。

まとめ

  • PreToolUse フックは権限チェックより先に発火し、denybypassPermissions でも効く
  • 止め方は 2 通り。短く書くなら exit 2、理由を渡すなら permissionDecision の JSON
  • 同じイベントに複数のフックを並べると並列実行され、拒否が優先される
  • フックの allow では deny ルールを上書きできない。強められるが緩められない
  • 文字列一致は rm -r -f や変数展開で外れる。確実な遮断は deny ルールとサンドボックスに任せる

よくある質問

PreToolUse フックは bypassPermissions でも効きますか?
効きます。公式ドキュメントは、PreToolUse フックがすべての権限モードで権限チェックより先に発火し、permissionDecision: deny を返せば bypassPermissions や --dangerously-skip-permissions でもツールをブロックすると明記しています。
フックで allow を返せば deny ルールを上書きできますか?
できません。公式ドキュメントは「フックは制限を強められるが、権限ルールが許す範囲を超えて緩めることはできない」と説明しています。
終了コード 2 と permissionDecision のどちらを使うべきですか?
理由を Claude に伝えたいなら permissionDecision の JSON、短く書きたいなら終了コード 2 です。どちらもブロックできます。
rm -rf の文字列一致だけで十分ですか?
不十分です。rm -r -f のような書き方や変数展開では一致しません。フックは運用の歯止めとして使い、確実な遮断は permissions の deny とサンドボックスに任せてください。

参考にした一次情報

この記事は公式ドキュメントを基に AI が下書きを作成し、運営者が内容を確認して公開しています。誤りを見つけた場合は お問い合わせからお知らせください。