Claude Media
PermissionRequest hookで承認ダイアログの前に判定を挟む

PermissionRequest hookで承認ダイアログの前に判定を挟む

PermissionRequestフックは権限確認ダイアログの直前に発火し、独自ロジックで許可・拒否を決められます。PreToolUse・PermissionDeniedとの違いと実装例をまとめます。

PermissionRequestフックは、Claude Codeがツール実行の許可をユーザーに尋ねようとする直前に発火するフックです。フックがdecisionオブジェクトを返せば、確認ダイアログを一度も表示せずに許可・拒否を確定できます。独自の承認ロジックを既存の権限ルールの手前に挟みたいとき、PreToolUseではなくこのイベントを使います。

PermissionRequestフックが発火するタイミング

PermissionRequestは「Claude Codeがこれから権限確認ダイアログを出そうとしている」瞬間にだけ発火します。プロンプトを表示できないセッション、たとえば非対話モードで動くバックグラウンドのサブエージェントでも同様に発火し、どのフックも決定を返さなければツール呼び出しは自動的に拒否されます。

matcherはツール名で絞り込め、値はPreToolUseと同じです。tool_use_idを含まない点がPreToolUseとの入力差で、これは同じツール呼び出しに対して両方のフックが発火しうるため、識別子で紐づける設計にしていないことの表れです。

PermissionRequestはサンドボックス化されたコマンドのネットワークリクエストに対しては発火しません。その種の確認をフックで捕まえたい場合は、Notificationフックのpermission_promptタイプを使います。EndConversationツールについてもPreToolUsePermissionRequestのどちらも発火しません。

PreToolUse・PermissionRequest・PermissionDeniedの違い

3つのフックはどれもツール呼び出しに絡みますが、発火条件とできることが異なります。混同すると「denyを返したのに効かない」ようなつまずきにつながります。

フック発火条件主にできることexit code 2の効果
PreToolUse発火条件権限の要不要にかかわらず、ツール呼び出しのたびに発火主にできることallow / deny / ask / defer、入力の書き換えexit code 2の効果ツール呼び出しをブロック
PermissionRequest発火条件Claude Codeがユーザーに確認しようとする直前だけ発火主にできることユーザーに代わってallow / denyを確定、入力の書き換え、権限ルールの追加exit code 2の効果無効(decisionオブジェクトが必須)
PermissionDenied発火条件autoモードが拒否したときだけ発火(手動でダイアログを拒否した場合・PreToolUseがブロックした場合・denyルールに一致した場合は発火しない)主にできることretry: trueでClaudeに再試行の余地を伝えるexit code 2の効果拒否は既に起きているため無視される

PreToolUseは許可が要らない呼び出し(すでにallowルールに一致するコマンド等)でも毎回起動するため、判定コストがかさみがちです。PermissionRequestは「本当にユーザー確認が必要になった呼び出し」だけに絞れるので、承認フローそのものをカスタマイズしたいときはこちらが軽量です。

PermissionDeniedの発火条件は特に見落としやすいところです。autoモードがガードレールに沿って自動で拒否したケースだけを対象にしているため、ユーザーが確認ダイアログで手動でDenyを押した場合や、PreToolUseフックが自らブロックした場合、あるいはdenyルールへの一致で確定した場合は、このフックはそもそも発火しません。「拒否をすべて捕捉したい」という前提でこのフックだけを実装すると、実際には大半の拒否を取りこぼすことになります。

受け取るJSONと返せる決定

PermissionRequestフックはtool_nametool_inputPreToolUseと同じ形で受け取り、加えてpermission_suggestionsという配列を受け取ります。これはユーザーがダイアログで選べる「常に許可」の選択肢に相当するデータです。

{
  "session_id": "abc123",
  "cwd": "/Users/you/project",
  "permission_mode": "default",
  "hook_event_name": "PermissionRequest",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf node_modules",
    "description": "Remove node_modules directory"
  },
  "permission_suggestions": [
    {
      "type": "addRules",
      "rules": [{ "toolName": "Bash", "ruleContent": "rm -rf node_modules" }],
      "behavior": "allow",
      "destination": "localSettings"
    }
  ]
}

返す側はhookSpecificOutput.decisionオブジェクトに5つのフィールドを詰められます。

フィールド用途
behavior用途"allow"で許可、"deny"で拒否。ただしdeny/askルールは引き続き評価されるため、allowを返してもマッチするdenyルールは上書きできない
updatedInput用途allow時のみ有効。ツールの入力パラメータを丸ごと置き換える(変更しないフィールドも含めて指定する)
updatedPermissions用途allow時のみ有効。allowルールの追加やセッションの権限モード変更を適用する
message用途deny時のみ有効。拒否理由をClaudeに伝える
interrupt用途deny時のみ有効。trueにするとClaudeを停止させる

if条件でさらに絞り込む

PermissionRequestifフィールドにも対応しています。ifが評価されるのはPreToolUsePostToolUsePostToolUseFailurePermissionRequestPermissionDeniedの5イベントだけで、それ以外のイベントにifを書いても発火しません。if"Bash(git *)"のような権限ルール構文でツール名と引数をまとめて評価するため、matcherでBashに絞ったうえにifを重ねれば「gitコマンドのときだけ承認ロジックを起動する」といった二段の絞り込みができます。

ifの判定はベストエフォートです。$()やバッククォートによるコマンド置換、変数展開でコマンド名が動的に決まる場合はClaude Codeが中身を確定できず、その場合はifがあってもフックを起動して念のため判定に回します。確実な許可・拒否そのものは権限システムのdeny/askルールに任せ、フックは判定の自動化・補助として使うのが安全です。

権限ルールをどこに書き込むか

updatedPermissionsと入力のpermission_suggestionsは、どちらも同じ形の「permission update entry」を使います。typeによって効果が変わり、destinationが書き込み先を決めます。

type効果
addRules効果ルールを追加する
replaceRules効果指定したbehaviorのルールを丸ごと置き換える
removeRules効果一致するルールを削除する
setMode効果セッションの権限モードを変更する(default / auto / acceptEdits / dontAsk / bypassPermissions / plan / manualmanualdefaultの別名で、v2.1.200以降が必要)
addDirectories効果作業ディレクトリを追加する
removeDirectories効果作業ディレクトリを削除する

destinationsession(メモリ上のみ、セッション終了で消える)・localSettings(.claude/settings.local.json)・projectSettings(.claude/settings.json、チーム共有)・userSettings(~/.claude/settings.json)の4種類です。個人の作業パターンに合わせた自動承認ならlocalSettings、チーム全体で共有したいルールならprojectSettingsに書くというように使い分けます。

Workspace trustとフックのセキュリティ

インタラクティブセッションでは、フォルダのworkspace trustダイアログを承認するまで、~/.claude/settings.jsonを含むすべての設定ファイル由来のフックが保留されます。一方-pフラグやSDK経由のセッションではこのダイアログ自体が出ず、フォルダは信頼済み扱いになるため、自分で書いた覚えのないリポジトリの.claude/settings.jsonにあるフックもそのまま実行されます。第三者のリポジトリに対してclaude -pをスクリプトから回す前には、.claude/配下の設定を確認するか--settings '{"disableAllHooks": true}'でフックを止めておく手があります。

PermissionRequestフックはユーザーに代わって許可を出せる強力な機構である以上、判定ロジックの不備がそのまま「本来ダイアログで止まるはずだった操作の素通り」につながります。コマンドフックはユーザーの権限でシェルコマンドを実行するため、次の4点は最低限守ります。

  • tool_inputの値を無条件に信用せず、判定前に検証する
  • シェル変数は"$VAR"のように必ずクォートする
  • ファイルパスの..によるパストラバーサルを弾く
  • .env.git/・鍵ファイルなど機微なパスは自動承認の対象から外す

実装例:特定コマンドを自動承認し、危険な操作は自動拒否する

npm run lintのような安全な決まりコマンドは常に許可し、rm -rfのような破壊的コマンドはダイアログを出さずに拒否するスクリプトです。

#!/bin/bash
# .claude/hooks/permission-gate.sh
input=$(cat)
command=$(jq -r '.tool_input.command // empty' <<<"$input")
 
if [[ "$command" == "npm run lint" ]]; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PermissionRequest",
      decision: {
        behavior: "allow",
        updatedPermissions: [
          {
            type: "addRules",
            rules: [{toolName: "Bash", ruleContent: "npm run lint"}],
            behavior: "allow",
            destination: "localSettings"
          }
        ]
      }
    }
  }'
elif [[ "$command" == rm\ -rf* ]]; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PermissionRequest",
      decision: {
        behavior: "deny",
        message: "rm -rfは自動拒否の対象コマンドです",
        interrupt: false
      }
    }
  }'
else
  exit 0  # 決定なし。通常の確認ダイアログに委ねる
fi

updatedPermissionsdestination: "localSettings"を指定すると、このルールは.claude/settings.local.jsonに書き込まれ、次回以降は同じコマンドで確認自体が発生しなくなります。sessionにすればセッション終了時に破棄され、projectSettingsにすればチームで共有する.claude/settings.jsonに残ります。

設定側はPermissionRequestイベントに紐づけるだけです。

{
  "hooks": {
    "PermissionRequest": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/permission-gate.sh"
          }
        ]
      }
    ]
  }
}

permission_suggestionsをそのまま採用する

自分でルールを組み立てなくても、フックが受け取ったpermission_suggestionsの要素をそのままupdatedPermissionsに流し込めます。これはユーザーがダイアログの「常に許可」を選んだのと同じ効果です。

jq -n --argjson sug "$(jq '.permission_suggestions[0]' <<<"$input")" '{
  hookSpecificOutput: {
    hookEventName: "PermissionRequest",
    decision: {behavior: "allow", updatedPermissions: [$sug]}
  }
}'

信頼できるパターン(社内で定義したコマンド一覧など)にだけこの経路を使い、permission_suggestionsを無条件で採用しない使い方が安全です。

落とし穴

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

behavior: "allow"を返しても、既存のdeny/askルールに一致していれば拒否や確認が優先されます。フック側のallowは「まだどのルールにも一致していない場合の判断」として働きます。

非対話セッションでは既定が拒否

プロンプトを出せないセッションで、どのフックもdecisionを返さなければツール呼び出しは自動的に拒否されます。無人実行のパイプラインで想定より多くの呼び出しが失敗する場合、まずこの既定挙動を疑います。

サンドボックス化コマンドのネットワークリクエストは対象外

サンドボックス経由のネットワークアクセス確認はこのフックを経由しないため、把握したいならNotificationpermission_promptを併用します。

bypassPermissionsへのsetModeは無条件では効かない

updatedPermissionssetModebypassPermissionsを指定しても、セッションが--dangerously-skip-permissions等ですでにbypassモードを使える状態で起動していなければ何も起きません。

よくある質問

PreToolUseで拒否すればPermissionRequestは不要ではないですか

拒否だけならどちらでも実現できます。PreToolUseも入力の書き換え(updatedInput)や文脈の追加(additionalContext)はできますが、updatedPermissionsによって「次回から確認なしで許可する」という権限ルールを追加できるのはPermissionRequestだけです。ユーザーに代わって承認履歴を残しながら判定したいならPermissionRequest一択になります。

exit code 2で拒否を表現できますか

できません。PermissionRequestイベントではexit code 2は無視され、hookSpecificOutput.decisionオブジェクトを返すことが唯一の手段です。

updatedInputで書き換えた入力は再度権限チェックされますか

されます。updatedInputで変更した入力は、deny/askルールに対して再評価されるため、書き換え後の値が別のdenyルールに一致すれば拒否に回ります。

複数のPermissionRequestフックを登録した場合はどうなりますか

マッチした全ハンドラが並列実行されます。複数のハンドラがそれぞれ異なるdecisionを返した場合の優先順位は公式ドキュメントで明示されていないため、判定ロジックは1つのハンドラに集約する設計が安全です。

プロンプトタイプのフックでも許可・拒否を決められますか

type: "prompt"type: "agent"のフックもPermissionRequestイベントで使えますが、モデルが返すok: falseはこのイベントでは効果を持ちません。ユーザーに代わって拒否を確定させたい場合は、コマンドフックからhookSpecificOutput.decision.behavior: "deny"を返す実装にする必要があります。

まとめ

PermissionRequestフックは、確認ダイアログが出る直前という一点に絞って独自の承認ロジックを挟める仕組みです。PreToolUseのような広い網ではなく、実際にユーザー確認が必要になった呼び出しだけを対象にできるため、社内の決まりコマンドを自動承認したり、危険な操作を無人実行の途中で確実に止めたりする用途に向いています。exit code 2が効かない点と、deny/askルールをallowで上書きできない点の2つを押さえておけば、実装で詰まる場面はほとんどありません。より広いHooksの全体像9つの実例レシピ、権限ルールそのものの管理は/permissionsコマンドもあわせて参照してください。

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