PermissionDenied(パーミッションデナイド)

フック
PermissionDenied
パーミッションデナイド
Claude Codeのauto modeで、内蔵の自動判定(classifier)が命令を却下した瞬間に発火する事後通知のしくみ。却下された命令の中身(tool_nameやtool_input、session_id、作業フォルダなど)を受け取り、こちらで用意したスクリプトで自由に処理できる。

auto modeを本番運用していて自動却下のログを取りたい中級〜上級の開発者向け

auto modeを本番運用していて、classifierに自動却下された命令の中身をログに残したい場面や、安全だと判明している命令だけAIにretry(もう一度試す)を打診したい場面で、settings.jsonにPermissionDenied登録を追加して使う。

Claude Codeをauto modeで動かしていると、AIが「rm node_modules を叩きたい」と言い出した瞬間に、内蔵のclassifierが「これは危ないから却下」と判断して止めてくれる場面があります。便利な仕組みですが、ログがどこにも残らないので、後から「いつ・なんで止められたのか」が追えない問題があります。

PermissionDeniedは、その却下が起きた直後にだけ走る「事後通知のしくみ」です。AIに対して「いや、これは安全だから、もう一回試して」と打診を返すこともできます。

噛み砕くと

家のスマートロックを思い浮かべると分かりやすいです。来客が解錠コードを間違えて入れた瞬間、ロックは無言で拒否します。誰が・いつ・どのコードを試したかは、別途ログを残す機械が必要です。

PermissionDeniedはその「ログを残す機械」にあたります。auto modeのclassifierが「却下」と判断した直後に、こちらが用意した小さな処理が走って、却下された命令の中身を覗ける。覗いた結果「これは誤判定だ、本当は安全だ」と判断すれば、AIに「もう一回どうぞ」と返せます。

大事な前提:このしくみは「auto modeの自動却下」でしか走らない

名前から「全部の拒否で走るのかな」と思いがちですが、走る場面はかなり狭いです。具体的には次の3つの状況のうち、最初の1つだけがPermissionDeniedの発火対象になります。

  • 発火する: auto modeを有効にしていて、classifierが命令を見て「これは却下」と自動判断した瞬間
  • 発火しない: 人間が手動で「Allow / Deny」ダイアログを見て「Deny」を選んだ瞬間。これはPermissionRequest側で扱う領域
  • 発火しない: PreToolUseフックが permissionDecision: "deny" を返して止めた瞬間。自分で書いたフックが拒否しているので、二重発火しないようになっている

つまり「Claude Codeの全拒否ログ」みたいな汎用ロガーにしようとすると、auto mode以外の拒否はまるごと抜け落ちます。auto modeを本番運用していて、その自動判定だけを観察したい場面のためのしくみです。

「cooking-blog で Bash(rm node_modules) が止められた」を例に、実際の手順を見る

料理ブログを作る用の cooking-blog プロジェクトをauto modeで動かしていた、という想定で進めます。Claudeがビルド作業の途中で「いったん node_modules を消して入れ直したい」と判断し、Bash(rm node_modules) を実行しようとしたら、classifierに却下されました。

ステップ1: フックを登録する場所を決める

ユーザー単位の設定ファイル ~/.claude/settings.json か、プロジェクト単位の .claude/settings.json のどちらかに登録します。今回は cooking-blog にだけ仕込みたいので、プロジェクト側に書きます。

ステップ2: 設定ファイルに「Bashの拒否で発火する」と書く

登録の中身はこんな形になります。matcher でどの種類の命令を拾うか絞り込めて、ここでは Bash 命令だけを対象にしています。

{
  "hooks": {
    "PermissionDenied": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "~/cooking-blog/.claude/scripts/denied-watcher.sh" }
        ]
      }
    ]
  }
}

ステップ3: 発火したときに走るスクリプトを書く

登録した denied-watcher.sh の中身を作ります。Claude Codeはこのスクリプトに、却下された命令の情報を入力として渡してきます。中身は次のような形のテキストデータです。

{
  "session_id": "abc123",
  "transcript_path": "~/.claude/projects/cooking-blog/transcript.jsonl",
  "cwd": "~/cooking-blog",
  "permission_mode": "auto",
  "hook_event_name": "PermissionDenied",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm node_modules"
  }
}

スクリプト側でこれを読んで、命令の中身を判定し、安全だと判断したらAIに「retryしていいよ」と返します。雛形はこんな感じになります。

#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')

# 「node_modules を消すだけ」なら retry を許す
if [[ "$cmd" == "rm node_modules" ]]; then
  echo '{
    "hookSpecificOutput": {
      "hookEventName": "PermissionDenied",
      "retry": true
    }
  }'
fi

exit 0

ステップ4: 実際に走らせて確認する

auto modeで cooking-blog を立ち上げて、Claudeにビルド作業を頼みます。途中で rm node_modules が叩かれて却下が起きると、登録したスクリプトが走り、stdoutにretry: trueが出力されて、AI側でもう一度同じ命令が実行されます。ここで初心者がよくやる勘違いがあります。「却下を最初から起こさない」設定だと思い込んでスクリプトに exit 0 以外の終了コードを返してしまうやつです。後述しますが、ここでは終了コードは何の意味も持ちません。

ステップ5: ログとして使う場合の運用

retryを返さず、却下の中身だけを記録する用途でも使えます。スクリプト末尾を「retryブロックを返さずファイルに追記するだけ」に変えれば、auto mode運用中の自動却下履歴が手元に残ります。cooking-blog/.claude/denied.log のような場所に tool_nametool_input.commandsession_id を時刻つきで書き出しておくと、週次レビューでパターン分析できます。

ステップ6: matcherを増やす

Bashだけでなく EditやWriteの却下も拾いたい場合、設定の matcher"Edit|Write" に変えます。Model Context Protocol経由の外部接続ツールの却下を拾うなら "mcp__.*" と書きます。複数を1つの登録で扱えるので、用途別にスクリプトを分けるとメンテしやすいです。

つまり PermissionDenied は何をしてくれるのか

  • やってくれる: auto modeのclassifierが却下した瞬間に、却下された命令の中身(tool_nametool_inputsession_id、作業フォルダなど)を受け取って、こちらのスクリプトで自由に処理する
  • やってくれる: 「これは誤判定だ」と判断したときだけ、retry: true をstdoutに返してAIに「同じ命令をもう一度試す」よう打診する
  • やってくれない: 却下を取り消すこと。AIが命令を実行する許可を「allow」に書き換えることはできず、できるのはretryの打診だけ
  • やってくれない: 手動Denyや、自分で書いたPreToolUseフックでの拒否の捕捉。このしくみは auto mode 自動却下の事後通知に特化している
  • 意味が薄い場面: そもそも auto mode を使っていないチーム。発火条件がない以上、登録しても何も起きない

使いどころ3シナリオ(具体題材で再現)

シナリオ1: 料理ブログを作っている開発者が、誤判定の傾向を可視化したいとき

cooking-blogをauto modeで運用していると、ビルド時の中間ファイル削除や、テスト用の一時フォルダ生成が、毎回classifierに止められて作業が中断する事態に出くわします。PermissionDeniedで tool_input.command を全部 denied.log に書き出しておくと、1週間で「自動却下の8割が rmchmod 系」みたいな傾向が見えます。そこに絞ってPreToolUse側で許可ルールを足せば、auto modeでの停止頻度を下げられます。

シナリオ2: 家計簿アプリのCI/CDで、安全と判明している命令だけ自動retryさせたいとき

家計簿アプリ kakeibo-app をauto modeで自動デプロイ運用しているとして、デプロイスクリプトが npm install --production を叩くたびにclassifierが止めにくる、という現象が起きるとします。これは安全な命令だと分かっているので、PermissionDeniedで tool_input.command を見て、その内容が npm install --production ぴったりだったら retry: true を返す処理を書きます。これでデプロイは中断せず、それ以外の npm publish のような命令は止めっぱなしにできます。

シナリオ3: OSS clone直後にauto modeで触る初日、却下傾向を観察するとき

初めてのOSSプロジェクトをローカルに落としてきて、auto modeでClaudeに「全体構造を読んで」と頼む初日に、何が止まるか分からない状態で走らせるのが怖い、という場面があります。PermissionDeniedで全Bash却下を denied.log に流すだけにしておけば、retryは一切返さず、その日の自動却下の全パターンを観察用に貯められます。翌日以降「これは止まると困る」を吟味してから許可ルールを足せるので、初日が安全に運用できます。

初心者が踏みやすい落とし穴

  • auto modeを有効にしていない環境では一切発火しない。手動Allow/Denyだけで運用しているなら、PermissionDenied側にいくら設定を書いても永遠に呼ばれない。発火しているか確認したい時は、まず auto mode の有効化を見直す
  • スクリプトの終了コードは無視されるexit 2 を返してエラー扱いにさせよう、という発想は通らない。却下は既に起きているので、Claude Codeはスクリプトの終了コードもエラー出力も読まない。唯一効くのは stdout に hookSpecificOutput.retry: true を出すこと
  • retryを返しても「無条件で実行」にはならない。あくまでAIに「もう一度試していいよ」と打診するだけで、Claudeが同じ命令を再度組み立てて実行しようとすると、もう一度classifierの判定を通る。同じ却下条件なら無限ループに見える挙動になりかねないので、retryを返す条件はかなり狭く書く
  • 名前が似た PermissionRequest と完全に別物。PermissionRequestは「許可ダイアログを出す前」に発火して、こちら側でallow/denyを返せる。PermissionDeniedは却下が起きた「あと」に発火する。仕事内容が真逆なので登録先を間違えると意図と全然違う挙動になる
  • matcher の書き方は他の tool 系フックと共通。PreToolUse、PostToolUse、PostToolUseFailure、PermissionRequest と同じ書式で、Bash 単体、Edit|Write の縦棒つなぎ、mcp__.* の点アスタリスクが使える。tool_name と完全一致ではなくパターン一致なので、絞り込みすぎに注意
  • session_id と transcript_path は秘密情報を含み得る。受け取ったまま外部サービスに送らない。ログにする場合も、置き場所をプロジェクト内に閉じる前提で運用する
  • PreToolUseで自分が拒否した命令はここに来ない。PreToolUse側で permissionDecision: "deny" を返した拒否は、自分の意図的なブロックなのでPermissionDeniedとして再通知されない。手動Denyも同じく対象外

書き方

// .claude/settings.json or ~/.claude/settings.json
{
  "hooks": {
    "PermissionDenied": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "~/your-project/.claude/scripts/denied-watcher.sh" }
        ]
      }
    ]
  }
}

やってみるとこうなる

入力

{
  "session_id": "abc123",
  "transcript_path": "~/.claude/projects/cooking-blog/transcript.jsonl",
  "cwd": "~/cooking-blog",
  "permission_mode": "auto",
  "hook_event_name": "PermissionDenied",
  "tool_name": "Bash",
  "tool_input": { "command": "rm node_modules" }
}

出力例

// スクリプトがstdoutに以下を出力すると、Claudeに「もう一度試していい」と打診できる
{
  "hookSpecificOutput": {
    "hookEventName": "PermissionDenied",
    "retry": true
  }
}

このページに出てきた言葉

auto mode
毎回「これ実行していい?」と人間に確認せず、Claude Codeが自分で判断して作業を進めるモード
classifier
auto modeが裏で動かしている「この命令は安全か危険か」の自動判定のしくみ
フック
Claude Codeの動作の節目で、こちらが用意した小さなプログラムを差し込めるしかけ
matcher
フックを「どの種類の命令で発火させるか」絞り込む条件。<code>Bash</code> や <code>Edit|Write</code> のようにパターンで書ける
tool 呼び出し
ClaudeがBashやEdit、Write、Readなどの道具を1回使う動作のこと
hookSpecificOutput
フックがClaude Codeに「こう振る舞ってほしい」と伝えるための返答ブロック。PermissionDeniedでは <code>retry: true</code> だけ指定できる
retry
「もう一度だけ試していい」とClaudeに伝える指示。<code>true</code> を返すと、ClaudeはさっきDenyされた命令をもう一度組み立てて実行しようとする
stdout
スクリプトが <code>echo</code> や <code>print</code> で吐き出す出力。Claude Codeはここに書かれた内容を読んでフックの応答として解釈する
settings.json
Claude Codeの動き方を書いておく設定ファイル。<code>~/.claude/</code> に置けば全プロジェクト共通、<code>.claude/</code> をプロジェクト直下に置けばそのプロジェクト限定

関連項目

公式ドキュメント

https://code.claude.com/docs/en/hooks

-

← 戻る