PermissionRequest hookで承認ダイアログの前に判定を挟む
PermissionRequestフックは権限確認ダイアログの直前に発火し、独自ロジックで許可・拒否を決められます。PreToolUse・PermissionDeniedとの違いと実装例をまとめます。
PermissionRequestフックは、Claude Codeがツール実行の許可をユーザーに尋ねようとする直前に発火するフックです。フックがdecisionオブジェクトを返せば、確認ダイアログを一度も表示せずに許可・拒否を確定できます。独自の承認ロジックを既存の権限ルールの手前に挟みたいとき、PreToolUseではなくこのイベントを使います。
PermissionRequestフックが発火するタイミング
PermissionRequestが走るのは、ダイアログを出そうとする瞬間か、プロンプトを出せないために自動拒否しようとする瞬間です。後者の典型が、非対話モードで動くバックグラウンドのサブエージェントです。この場合もフックは実行され、どのフックも決定を返さなければツール呼び出しは拒否されます。
--permission-prompt-toolやAgent SDKのcanUseToolコールバックに行き着く呼び出しでは、フックとホスト側が並走し、先に決めた方が採用されます。
matcherはツール名で絞り込め、値はPreToolUseと同じです。入力にはMCPツールならmcp_serverオブジェクトも含まれますが、tool_use_idは含まれません。PreToolUseの入力と見比べるとき、この1項目だけ欠けています。
PermissionRequestは、サンドボックス化されたコマンドのネットワークリクエストに対しては発火しません。その種の確認をフックで捕まえたい場合は、Notificationフックのpermission_promptタイプを使います。EndConversationツールについてもPreToolUse・PermissionRequestのどちらも発火しません。発火条件や終了後のロックなど、権限やフックで止められない理由の詳細はEndConversationツールの挙動で扱っています。
3つのフックの違い
3つのフックはどれもツール呼び出しに絡みますが、発火条件とできることが異なります。混同すると「denyを返したのに効かない」ようなつまずきにつながります。
PreToolUse・PermissionRequest・PermissionDenied
PreToolUse
権限の要不要にかかわらず、ツール呼び出しのたびに発火します。allow / deny / ask / deferを返せ、入力も書き換えられます。exit code 2はツール呼び出しをブロックします。
PermissionRequest
Claude Codeがユーザーに確認しようとするときだけ発火します。ユーザーに代わってallow / denyを確定でき、入力の書き換えと権限ルールの追加もできます。exit code 2は無視され、decisionオブジェクトが必須です。
PermissionDenied
autoモードが拒否したときだけ発火します。retry: trueを返すと、Claudeに再試行してよいと伝えられます。拒否そのものは覆りません。
PreToolUseは許可が要らない呼び出し(すでにallowルールに一致するコマンド等)でも毎回起動するため、発火回数が多くなります。PermissionRequestは「本当にユーザー確認が必要になった呼び出し」だけに絞れるので、承認フローそのものをカスタマイズしたいときに向きます。スラッシュコマンドを直接タイプした時点で止めたいなら、権限確認より前に発火するUserPromptExpansion hookの方が適しています。
PermissionDeniedの発火条件は特に見落としやすいところです。次の拒否では発火しません。
- ユーザーが確認ダイアログで手動でDenyを押した
PreToolUseフックがブロックした- denyルールに一致して確定した
「拒否をすべて捕捉したい」という前提でこのフックだけを実装すると、手動のDeny・フックによるブロック・denyルールでの拒否を取りこぼします。auto modeの拒否後に再試行させるretryの使い方はPermissionDenied hookで扱っています。
受け取るJSONと返せる決定
PermissionRequestフックはtool_nameとtool_inputをPreToolUseと同じ形で受け取り、さらにpermission_suggestions配列も受け取ります。これはClaude Codeが提案する権限更新のリストで、ダイアログの選択肢のもとになる提案の一覧です。
{
"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"
}
]
}permission_suggestionsはダイアログの選択肢と完全には一致しません。ファイル編集のダイアログのようにこの配列を読まず、リクエストから選択肢を作るものがあります。管理設定のallowManagedPermissionRulesOnlyがルール保存の選択肢を隠していても、配列には提案が残ることがあります。逆に「Yes, and switch to auto mode」のように、提案エントリを持たない選択肢もあります。配列は「提案の一覧」であり、画面の写しではありません。
返す側はhookSpecificOutput.decisionオブジェクトに5つのフィールドを詰められます。
| フィールド | 用途 |
|---|---|
behavior | 用途"allow"で許可、"deny"で拒否。deny/askルールは引き続き評価され、allowを返してもマッチするdenyルールは上書きできない |
updatedInput | 用途allow時のみ有効。ツールの入力パラメータを丸ごと置き換える(変更しないフィールドも含めて指定する) |
updatedPermissions | 用途allow時のみ有効。allowルールの追加やセッションの権限モード変更を適用する |
message | 用途deny時のみ有効。拒否理由をClaudeに伝える |
interrupt | 用途deny時のみ有効。trueにするとClaudeを停止させる |
if条件でさらに絞り込む
PermissionRequestはifフィールドにも対応しています。ifが評価されるのはPreToolUse・PostToolUse・PostToolUseFailure・PermissionRequest・PermissionDeniedの5イベントだけです。それ以外のイベントにifを書くと、フックは一度も実行されません。ifは"Bash(git *)"のような権限ルール構文でツール名と引数をまとめて評価します。matcherでBashに絞ったうえにifを重ねれば、「gitコマンドのときだけ承認ロジックを起動する」といった二段の絞り込みになります。
ifの判定はベストエフォートです。コマンドが何を実行するか確定できない場合は、パターンに合わなくてもフックが起動します。確実な許可・拒否そのものは権限システムのdeny/askルールに任せ、フックは判定の自動化・補助として使うのが安全です。
実装して動かす
PermissionRequestフックの導入
- 1
判定スクリプトを置く
.claude/hooks/permission-gate.shを作り、chmod +xで実行権限を付けます。中身は次の節にあります。 - 2
settings.jsonにフックを登録する
PermissionRequestイベントにmatcherBashでスクリプトを結びます。設定例は実装例の後ろに載せています。 - 3
標準入力にJSONを流して試す
Claude Codeを起動する前に、サンプルのJSONをスクリプトへパイプして出力を見ます。
特定コマンドを自動承認し、危険な操作は自動拒否する
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設定側はPermissionRequestイベントに紐づけるだけです。
{
"hooks": {
"PermissionRequest": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/permission-gate.sh"
}
]
}
]
}
}作業ディレクトリに置いたこのスクリプトへ、Bashツールの入力を模したJSONを標準入力から流して試しました。モデルは呼んでいません。
$ echo '{"tool_input":{"command":"npm run lint"}}' | .claude/hooks/permission-gate.sh | jq -c .
{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow","updatedPermissions":[{"type":"addRules","rules":[{"toolName":"Bash","ruleContent":"npm run lint"}],"behavior":"allow","destination":"localSettings"}]}}}
$ echo '{"tool_input":{"command":"rm -rf node_modules"}}' | .claude/hooks/permission-gate.sh | jq -c .
{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"rm -rfは自動拒否の対象コマンドです","interrupt":false}}}
$ echo '{"tool_input":{"command":"ls"}}' | .claude/hooks/permission-gate.sh3本目のlsは何も出力しないまま終了します。この「出力なし」がフックの返事で、通常の確認ダイアログがそのまま表示されます。
注意したいのは、前方一致で書いた拒否条件の抜けです。同じスクリプトにrm -fr x・sudo rm -rf x・スペースを2つ入れたrm -rf xを流すと、3つとも出力なしで終わりました。つまり拒否されず、ダイアログに回ります。rm -rfを完全に止めたいなら、フックの文字列一致ではなく権限システムのdenyルールを使います。このスクリプトの拒否は、典型的な書き方を先回りして断る補助です。
updatedPermissionsにdestination: "localSettings"を指定すると、ルールが保存され、次回以降、同じコマンドで確認は発生しません。書き込み先は4種類あり、どれを選ぶかで寿命と共有範囲が変わります。
destinationの4種類
session
メモリ上だけに持ち、セッションを終えると消えます。その場限りの許可に向きます。
localSettings
.claude/settings.local.jsonに書きます。個人の作業パターンに合わせた自動承認はここです。projectSettings
.claude/settings.jsonに書きます。チームで共有したいルールだけを置きます。userSettings
~/.claude/settings.jsonに書きます。プロジェクトをまたいで効きます。
permission_suggestionsをそのまま採用する
自分でルールを組み立てなくても、フックが受け取ったpermission_suggestionsの要素をそのままupdatedPermissionsに流し込めます。ダイアログの「常に許可」に近い効果になります。
jq -n --argjson sug "$(jq '.permission_suggestions[0]' <<<"$input")" '{
hookSpecificOutput: {
hookEventName: "PermissionRequest",
decision: {behavior: "allow", updatedPermissions: [$sug]}
}
}'この経路は、社内で定義したコマンド一覧のような信頼できるパターンにだけ使います。permission_suggestionsを無条件で採用すると、本来ダイアログで止まる操作が素通りします。
権限更新エントリの種類
updatedPermissionsと入力のpermission_suggestionsは、どちらも同じ形の権限更新エントリを使います。typeで効果が変わります。
| type | 効果 |
|---|---|
| addRules | 効果ルールを追加する。behaviorはallow・deny・askから選ぶ。ruleContentを省くとツール全体に一致する |
| replaceRules | 効果指定したdestinationの、指定したbehaviorのルールを丸ごと置き換える |
| removeRules | 効果一致するルールを削除する |
| setMode | 効果セッションの権限モードを変更する(default / auto / acceptEdits / dontAsk / bypassPermissions / plan / manual) |
| addDirectories | 効果作業ディレクトリを追加する |
| removeDirectories | 効果作業ディレクトリを削除する |
manualはdefaultの別名で、v2.1.200以降が必要です。v2.1.287のclaude --helpでは--permission-modeの選択肢にmanualが並びます。
$ claude --help | grep -A3 -- "--permission-mode"
--permission-mode <mode> Permission mode to use for the session
(choices: "acceptEdits", "auto",
"bypassPermissions", "manual",
"dontAsk", "plan")同じ--helpには--permission-prompts <target>もあります。-pで動かすとき、確認に誰が答えるかをhostかnoneで決めるオプションで、v2.1.259以降が必要です。noneなら確認が要る操作は自動で拒否されますが、PermissionRequestフックがallowを返した操作は通ります。
Workspace trustとフックのセキュリティ
インタラクティブセッションでは、フォルダのworkspace trustダイアログを承認するまで、~/.claude/settings.jsonを含むすべての設定ファイル由来のフックが保留されます。一方-pフラグやSDK経由のセッションではこのダイアログ自体が出ず、フォルダは信頼済み扱いになります。自分で書いた覚えのないリポジトリの.claude/settings.jsonにあるフックも、そのまま実行されます。
第三者のリポジトリに対してclaude -pをスクリプトから回す前には、次のいずれかが選べます。
.claude/配下の設定を確認する--bareで起動する--settings '{"disableAllHooks": true}'でフックを止める
PermissionRequestフックはユーザーに代わって許可を出せる強力な機構です。判定ロジックの不備は、そのまま「本来ダイアログで止まるはずだった操作の素通り」につながります。コマンドフックはユーザーの権限でシェルコマンドを実行するため、次の4点は最低限守ります。
tool_inputの値を無条件に信用せず、判定前に検証する- シェル変数は
"$VAR"のように必ずクォートする - ファイルパスの
..によるパストラバーサルを弾く .env・.git/・鍵ファイルなど機微なパスは自動承認の対象から外す
bypassPermissionsへのsetModeは無条件では効かない
updatedPermissionsでsetModeにbypassPermissionsを指定しても、セッションがバイパスモードを使える状態で起動していなければ何も起きません。バイパスモードを使える状態にするのは、次のいずれかです。
--dangerously-skip-permissionsで起動する--permission-mode bypassPermissionsで起動する--allow-dangerously-skip-permissionsで起動するpermissions.defaultMode: "bypassPermissions"をユーザー・--settings・管理設定のいずれかに書く
permissions.disableBypassPermissionsModeでモードが無効化されている場合や、制限モードで始まったセッションでも何も起きません。bypassPermissionsはdestinationにかかわらず、defaultModeとして保存されることはありません。
よくある質問
PreToolUseで拒否すればPermissionRequestは不要ではないですか
拒否だけならどちらでも実現できます(実装はPreToolUseフックの実装例にまとめました)。PreToolUseの出力はpermissionDecisionで許可・拒否・確認・保留を返す形で、権限ルールを書き込むフィールドはありません。updatedPermissionsで「次回から確認なしで許可する」ルールを追加できるのはPermissionRequestのdecisionだけです。
updatedInputで書き換えた入力は再度権限チェックされますか
されます。updatedInputで変更した入力は、deny/askルールに対して再評価されます。書き換え後の値が別のdenyルールに一致すれば拒否に回ります。
複数のPermissionRequestフックを登録した場合はどうなりますか
マッチした全ハンドラが並列実行されます。同じハンドラを複数の設定ファイルに書いた場合は1回だけ実行されます。ハンドラが別々のdecisionを返す構成は避け、判定は1つのハンドラにまとめます。Agent SDKのhooks解説では、複数のフックやルールが当てはまるときdenyがallowより優先します。PreToolUseではdeny、defer、ask、allowの順です。
プロンプトタイプやエージェントタイプのフックでも許可・拒否を決められますか
PermissionRequestが対応するフックの種類はcommand・http・mcp_tool・promptで、agentには対応しません。type: "agent"を登録しても実行されず、権限フローはそのまま進みます。type: "prompt"は使えますが、モデルが返すok: falseはこのイベントでは効果を持ちません。ユーザーに代わって拒否を確定させたい場合は、コマンドフックかHTTPフックからhookSpecificOutput.decision.behavior: "deny"を返します。
まとめ
確認が出る呼び出しの承認を自動化したいときはPermissionRequest、確実に止めたい操作は権限システムのdenyルール、という分担で組みます。
より広いHooksの全体像や9つの実例レシピ、権限ルールそのものの管理は/permissionsコマンドもあわせて参照してください。