Claude Media
PreToolUse hookでツール実行前に許可・拒否・改変する

PreToolUse hookでツール実行前に許可・拒否・改変する

PreToolUseフックはツール呼び出しのたびに発火し、許可・拒否・確認・保留の4通りの決定と入力の書き換えができます。受け取るJSONと実装の落とし穴をまとめます。

PreToolUseフックは、Claudeがツールの入力パラメータを組み立てた直後、そのツール呼び出しが実際に処理される前に発火します。権限プロンプトの要不要にかかわらず毎回起動するため、rm -rfのような破壊的なBashコマンドを止めたり、書き込み先のパスを検証したり、入力そのものを書き換えたりする用途に向きます。フックイベント全体の一覧はHooks完全ガイドにまとめてあるので、本記事はPreToolUseが受け取る情報と4通りの決定、実装で見落としやすい点に絞ります。

PreToolUseが発火するタイミング

PreToolUseEndConversationを除くすべてのツール名にマッチします。組み込みツール(BashEditWriteReadGlobGrepAgentWebFetchWebSearchAskUserQuestionExitPlanModeなど)はもちろん、mcp__<server>__<tool>の形式で登録されるMCPツールも対象です。

MCPツールをmatcherで絞り込むときは.*を省略できません。mcp__memoryのように英数字・アンダースコア・ハイフンだけで書くと完全一致として評価され、どのツール名とも一致しないため一つも発火しません。mcp__memory__.*と書いて初めてmemoryサーバーの全ツールにマッチします。プラグインが同梱するMCPサーバーの場合はツール名にプラグイン名が挟まりmcp__plugin_<プラグイン名>_<サーバー名>__<ツール名>という形になるので、matcherもこのスコープ付きの名前で書く必要があります。

見落としやすいのは@でファイルを参照したときの挙動です。プロンプト中で@によって参照したファイルは、Claude Codeがプロンプト組み立ての段階で内容を直接埋め込むため、ツール呼び出しを経由しません。ReadにマッチするPreToolUseフックを書いても、この経路のファイル読み込みは素通りします。特定のパスを@参照ごと止めたい場合は、フックではなくReaddenyルールを使います。

受け取るJSON

PreToolUsesession_idcwdpermission_modehook_event_nameなどの共通フィールドに加えて、tool_nametool_inputtool_use_idを受け取ります。Bashコマンドの場合は次のような形です。

{
  "session_id": "abc123",
  "cwd": "/Users/you/project",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "description": "Run test suite",
    "timeout": 120000,
    "run_in_background": false
  },
  "tool_use_id": "toolu_01ABC123..."
}

WriteEditReadではtool_input.file_pathが常に絶対パスで渡されます。Claude Codeがフック実行前に~や相対パスを展開済みなので、パス比較をすり抜けられる心配はありません。ただしWindowsでは区切り文字がバックスラッシュのまま届きます。Git Bash上で動くフックで$PWD/c/projectのように見えていても、tool_input.file_pathC:\\project\\src\\index.tsという形です。フォワードスラッシュ前提で/src/のような文字列比較を書くと一致せず、ブロックしたつもりのパスがすり抜けます。比較前にfile_path.replace("\\", "/")のように区切り文字をそろえ、絶対パスなので^アンカーではなく/src/のようなセグメント一致で判定するのが安全です。

4つの決定と優先順位

PreToolUsehookSpecificOutputオブジェクトの中にpermissionDecisionを返すことで、他のフックより細かい制御ができます。

permissionDecision効果
allow効果権限プロンプトをスキップする。ただしAskUserQuestionExitPlanModeは例外で、updatedInputと組み合わせる必要がある
deny効果ツール呼び出しを止める。理由はClaudeに見える
ask効果ユーザーに確認を求める
defer効果いったん処理を保留する(非対話モード専用)

複数のフックが異なる決定を返した場合の優先順位はdeny > defer > ask > allowです。deny/askの権限ルールは、フックの決定にかかわらず引き続き評価されるため、フック側がallowを返しても既存のdenyルールに一致すれば拒否が優先されます。

exit 2で終了する実装はdenyと同じ経路を通り、Claudeはstderrのメッセージを拒否理由として受け取ります。askを返すと、権限プロンプトに[settings](設定ファイルまたはagent frontmatter由来)・[plugin:<name>](プラグイン)・[skill](skill frontmatter)のいずれかのラベルが付き、どの設定がこの確認を要求しているかが分かるようになっています。auto modeでもaskは強制的にプロンプトを出させます。分類器がツール呼び出しを拒否することはできますが、フックが要求した確認を黙って承認することはできません。

実装例 — 危険なコマンドを止める

Bashif条件でrm *にマッチする呼び出しだけを絞り込み、rm -rfを含む場合にdenyを返すスクリプトです。設定側はmatcherでBash呼び出しに絞り、ifでさらにrm *だけに絞ることで、それ以外のコマンドではプロセスを起動せずに済みます。

#!/bin/bash
# .claude/hooks/block-rm.sh
COMMAND=$(jq -r '.tool_input.command')
 
if echo "$COMMAND" | grep -q 'rm -rf'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Destructive command blocked by hook"
    }
  }'
else
  exit 0  # 決定なし。通常の権限フローに委ねる
fi

設定は次のとおりです。argsを指定する実行形式(exec form)にするとシェルを介さず直接スクリプトを起動でき、${CLAUDE_PROJECT_DIR}のようなパスプレースホルダーをクォート忘れで壊す心配がありません。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(rm *)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
            "args": []
          }
        ]
      }
    ]
  }
}

ifフィールドはPreToolUsePostToolUsePostToolUseFailurePermissionRequestPermissionDeniedの5イベントだけで評価され、permissionsと同じルール構文を使います。判定はベストエフォートで、$()やバッククォートによるコマンド置換、$VARでコマンド名が動的に決まる場合はClaude Codeが中身を確定できないため、ifがあってもフックを起動して念のため判定に回します。確実な遮断そのものは権限システムのdeny/askルールに任せ、フックは補助として使うのが安全です。

updatedInputで入力を書き換える

updatedInputはツールの入力パラメータをまるごと置き換えます。変更しないフィールドも含めて指定しないと消えてしまう点に注意します。Claude Codeは、権限ルールとBashコマンドの自動バックグラウンド適格性を、Claudeが送った元の入力ではなくupdatedInputで返した入力に対して評価します。つまり書き換え後の値が別のdenyルールに一致すれば、allowを返していてもその後段で拒否に回ります。allowと組み合わせれば自動承認、askと組み合わせればユーザーに修正後の入力を見せてから確認できます。

AskUserQuestionExitPlanModeはこの仕組みの特殊なケースです。非対話モードの-pフラグ実行では、Agent SDKのcanUseToolコールバックのような「permission host」がない限りこの2つのツールを提供できません。フックがstdinから入力を読み、独自UIで回答を集めてupdatedInputに詰めてallowとともに返すことで、プロンプトなしにツールを実行させられます。AskUserQuestionの場合は元のquestions配列をそのまま返しつつ、質問文をキーにしたanswersオブジェクトを追加します。

deferで呼び出しを保留する

deferclaude -pをサブプロセスとして起動しJSON出力を読み取るような統合(Agent SDKアプリや独自UI)向けの決定です。非対話モードの-pフラグでだけ有効で、インタラクティブセッションでは警告を出して無視されます。典型的なのはAskUserQuestionで、ターミナルが無いのでその場では答えられない状況です。

  1. ClaudeがAskUserQuestionを呼び出し、PreToolUseが発火する
  2. フックがpermissionDecision: "defer"を返す。ツールは実行されず、stop_reason: "tool_deferred"でプロセスが終了し、保留中の呼び出しがtranscriptに残る
  3. 呼び出し元プロセスがSDK結果からdeferred_tool_useを読み取り、自前のUIで質問を表示して回答を待つ
  4. 同じpermission hostを付けてclaude -p --resume <session-id>を実行する。同じツール呼び出しが再びPreToolUseを発火させる
  5. フックがupdatedInputに回答を詰めてallowを返す。ツールが実行され、Claudeが続行する

deferはClaudeがそのターンで1回だけツールを呼び出した場合にのみ有効です。複数のツール呼び出しをまとめて行った場合は警告とともに無視され、通常の権限フローに戻ります。バッチの中から1件だけを保留して他を宙に浮かせる方法がないためです。タイムアウトや再試行回数の上限もなく、セッションはcleanupPeriodDaysの既定30日が過ぎるまでディスク上に残り続けます。

落とし穴

exec formとshell formの取り違え

argsを指定すると実行形式(exec form)になり、commandはPATH上の実行ファイルとして直接起動されるためシェルを介した引用符解釈が起きません。argsを省略するとシェル形式(shell form)になり、パイプや&&が使えますが、パス中のスペースや特殊文字はダブルクォートで囲む必要があります。パスプレースホルダーを使うフックは、原則としてexec formにしてargsへ渡すほうが安全です。

Windowsの.cmd/.batシム

npmやnpxがインストールするnode_modules/.bin配下の.cmd.batシムは実行ファイルではないため、exec formではシェルなしに起動できません。nodeを直接呼び出して対象スクリプトを渡すか、シェル形式でシムを名前で呼び出す必要があります。

複数ハンドラは並列実行される

同じイベントにマッチする複数のフックハンドラはすべて並列で実行されます。同じハンドラを複数の設定ファイルに定義しても1回しか実行されませんが、プラグインやskillが持つ同名ハンドラは別物として扱われ、それぞれ実行されます。

PreToolUseとPermissionRequestの使い分け

似た用途に見えるフックとの境界も押さえておくと実装で迷いません。

用途向くフック理由
権限の要不要に関係なく毎回検証したい向くフックPreToolUse理由呼び出しのたびに発火するため判定の抜け漏れがない
確認ダイアログが出る直前だけ処理を挟みたい向くフックPermissionRequest理由すでにallowルールに一致する呼び出しでは起動せず軽量
特定コマンドの失敗時だけリカバリしたい向くフックPostToolUseFailure理由ツール実行後の結果を見て判断するイベントのため
次回から確認なしで許可するルールを追加したい向くフックPermissionRequest理由updatedPermissionsで権限ルールそのものを書き込める

PreToolUseは「実行前に必ず通る関所」、PermissionRequestは「ユーザー確認の代行」という役割分担で覚えると使い分けやすくなります。

まとめ

PreToolUseはツール呼び出しのたびに発火する分、判定コストと引き換えに取りこぼしのない制御ができるフックです。permissionDecisionの4種類とその優先順位、updatedInputが権限ルールに対して再評価される仕組み、そして@参照やWindowsパス、タイムアウトの扱いといった細部を押さえれば、破壊的なコマンドを止めるスクリプトから非対話モードでの承認代行まで幅広く実装できます。ユースケース別の実装はHooks実例カタログもあわせて参照してください。

この記事を共有:XはてブLinkedIn