PostToolUseFailure / PermissionRequest(ポストツールユースフェイラー・パーミッションリクエスト)

フック
PostToolUseFailure / PermissionRequest
ポストツールユースフェイラー・パーミッションリクエスト
Claude Codeのツール実行まわりに割り込めるフック5兄弟のうち、後ろ寄りの2つ。PostToolUseFailureはツール実行が失敗した直後に呼ばれて「失敗の後始末・ログ・Claudeへの補足情報」を担当する。PermissionRequestは人間に「このコマンド許可しますか」のダイアログが出る直前に呼ばれて、自動で許可・拒否・コマンド書き換えを返せる。

Claude Code のフック設定を書こうとしているが、ツール周りのフック5兄弟(PreToolUse / PostToolUse / PostToolUseFailure / PermissionRequest / PermissionDenied)の使い分けが分からない人向け

PostToolUseFailureは「ツールが失敗した時に Slack 通知したい・失敗を Claude の次の手のヒントに変えたい」場面で <code>.claude/settings.json</code> に登録する。PermissionRequestは「危ない rm だけ人に確認させて、安全な npm test は素通りで通したい」「コマンドの中身を見て勝手に sanitize(安全な形に書き換え)してから許可したい」場面で同じく settings.json に登録して、JSON で decision.behavior を返すスクリプトを書く。

Claude Codeのフックには、Claudeがツールを叩こうとする瞬間に挟み込めるものが5種類ある。PreToolUse / PostToolUse / PostToolUseFailure / PermissionRequest / PermissionDenied の5兄弟だ。このページで扱うのはそのうち後ろの2つ。失敗を見届ける後始末役と、人間に「やっていい?」と聞かれる直前に割り込むゲート役です。

役割を1行で分けるとこう。PostToolUseFailure は「失敗した事実を観察してClaudeに次の手の材料を渡す」。PermissionRequest は「人間にダイアログが出る前に、自動でOK・NG・条件付きOKを決める」。止められるのは後者だけです。

噛み砕くと

会社で新人がコピー機を使う場面を思い浮かべます。新人が「この資料コピーしていいですか」と上司に確認してくる瞬間がある。これが PermissionRequest。先輩がそのやり取りを横から見て「いいよ」「ダメ」を代わりに即答できる。さらに「今後この種類の資料は黙ってコピーしていい」と社内ルールを書き足すこともできる。

一方、コピー機が紙詰まりで止まった瞬間に駆けつけてログを取り、「今度から両面印刷の時はこの紙種を避けて」と新人にメモを渡す係が PostToolUseFailure。紙詰まりそのものは止められない。すでに起きた失敗を観察して、次の指示の材料を作るだけです。

大事な前提:このフック2つは「同じツール呼び出しを別の瞬間で捕まえる」

5兄弟は tool_use_id という同じIDで紐付いている。1回のBash実行が PreToolUse(実行前)→ PermissionRequest(人間に出す直前)→ PostToolUse or PostToolUseFailure(実行後)と流れる中で、全イベントが同じ tool_use_id を持つ。後でログを突き合わせる時の鍵です。

そして表現力に決定的な差が出る部分が1つ。PermissionRequest は exit code 2 でも拒否扱いになるが、それだけだと message も updatedInput も permissionRules も渡せない。実用上はJSONで hookSpecificOutput.decision.behavior: "deny" を返して exit 0、というのが定石です。ユーザーへの説明文や、許可と引き換えにルールを書き足したい時はJSON経由じゃないと表現できない。

「ブログ用にClaude Codeで作業する時、危ない rm だけ人に確認させて、テスト失敗は Slack に飛ばす」を例に、実際の手順を見る

ブログ記事のソースを Claude Code 管理に乗せていて、Claudeに「画像を変換して」「テスト走らせて」「不要ファイル消して」などを頼む使い方を想定します。やりたいのは次の2つ。

  1. rm -rf だけは必ず人間が確認する。npm testnpm run build は黙って通す
  2. npm test が失敗した瞬間に、エラー内容を Slack に流す(人間が後で気づける)

ステップ1: 設定ファイルにフックを宣言する

プロジェクト直下の .claude/settings.json を開いて、hooks ブロックに2つのイベントを足します。

{
  "hooks": {
    "PermissionRequest": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/bash-gate.sh" }
        ]
      }
    ],
    "PostToolUseFailure": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/notify-test-failure.sh" }
        ]
      }
    ]
  }
}

これで Bash ツール(コマンドを叩くツール)で何か起きるたびに2つのスクリプトが呼ばれるようになります。

ステップ2: PermissionRequest 側のゲートを書く

.claude/hooks/bash-gate.sh をこう書きます。Claudeが叩こうとしたコマンドを tool_input.command で受け取って、内容を見て自動で許可・拒否を返す形です。

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

# rm -rf は人間に判断を委ねる: deny して message でユーザーに見せる
if echo "$cmd" | grep -qE '^rm[[:space:]]+-rf'; then
  jq -n --arg c "$cmd" '{
    "hookSpecificOutput": {
      "hookEventName": "PermissionRequest",
      "decision": {
        "behavior": "deny",
        "message": "rm -rf は自動承認しない。手動で打ち直してください: \($c)"
      }
    }
  }'
  exit 0
fi

# npm test / npm run build は黙って通す
if echo "$cmd" | grep -qE '^npm[[:space:]]+(test|run[[:space:]]+build)'; then
  jq -n '{
    "hookSpecificOutput": {
      "hookEventName": "PermissionRequest",
      "decision": { "behavior": "allow" }
    }
  }'
  exit 0
fi

# それ以外は何も返さない: 通常のダイアログを人間に見せる
exit 0

ポイントは exit code 2 ではなくJSON を返している こと。exit 2 だけでも拒否扱いにはなるけど、ユーザーに「なぜ自動承認しないか」を伝えるメッセージや、コマンドを書き換えて許可する updatedInput、以降聞かれなくなる permissionRules は全部JSONじゃないと書けない。ここで初心者がやりがちな勘違いがあって、他のフックの感覚で「拒否したいから exit 2 だけ返せばいいや」と書くと、deny は成立するんだけど何の説明もなく止まるのでClaudeが「なぜ通らない?」と混乱します。

ステップ3: 動作確認する

Claude Codeを立ち上げて、Claudeに「./node_modules を消してきれいにして」と頼みます。Claudeは rm -rf ./node_modules を叩こうとして、フックが拒否を返すので、ダイアログ自体が出ずに「rm -rf は自動承認しない」というメッセージが Claude 側に渡る。Claudeは「では手動で打ち直してください」と返してきます。

次に「テスト走らせて」と頼むと、Claudeは npm test を叩こうとして、フックが allow を返すのでダイアログは出ない。そのまま実行に進みます。

ステップ4: PostToolUseFailure 側の通知スクリプトを書く

テストが失敗した瞬間に Slack に流す係です。.claude/hooks/notify-test-failure.sh を次のように書きます。送信先のURLは SLACK_WEBHOOK_URL~/.zshrc~/.bashrcexport SLACK_WEBHOOK_URL='https://hooks.slack.com/services/xxx' の形で書いておくと、Claude Code を起動したターミナル側の設定値がそのまま引き継がれて読み取られます。

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

# npm test の失敗だけに反応
if echo "$cmd" | grep -qE '^npm[[:space:]]+test'; then
  curl -s -X POST -H 'Content-Type: application/json' \
    -d "{\"text\": \"npm test 失敗 (id=$tid): $err\"}" \
    "$SLACK_WEBHOOK_URL" >/dev/null

  # Claude 側にも次の手のヒントを渡す
  jq -n --arg e "$err" '{
    "hookSpecificOutput": {
      "hookEventName": "PostToolUseFailure",
      "additionalContext": "テストが失敗しました: \($e)。失敗ログをSlackに通知済み。next step として該当テストファイルを開いて原因を特定してください。"
    }
  }'
fi
exit 0

ここで重要なのは additionalContext。これを返すと、Claudeの次のターンに「テスト失敗しました、Slack通知済み、次は該当テストファイルを開いて」という文字列がClaudeの目の前に追加されます。失敗を「ただ観測する」だけじゃなくて、Claudeの次の手の材料を渡せる。

ステップ5: 失敗をブロックしようとして失敗する(観察)

もし PostToolUseFailure の中で exit 2 を返したらどうなるか。試すと、stderr の内容は Claude に「参考情報」として渡るけど、すでに失敗しているコマンドそのものは再実行されない。PostToolUseFailure は「失敗を止める」のではなく「失敗の後始末をする」フックだからです。これは仕様。

ステップ6: tool_use_id でログを突き合わせる

動作確認の後、ログファイルや Slack の履歴で tool_use_id を見比べると、同じBash実行の PreToolUsePermissionRequestPostToolUseFailure が同じIDで流れていることが確認できます。1個のツール呼び出しを最初から最後まで追えるので、複雑なフック群を組んだ時のデバッグはこのIDが頼り。

つまり PostToolUseFailure / PermissionRequest は何をしてくれるのか

  • PermissionRequest がやってくれる: 人間にダイアログが出る前に自動で許可・拒否を返す、コマンド内容を updatedInput で書き換えて安全な形にして許可する、permissionRules で「以降このパターンは聞かない」ルールを書き足す
  • PostToolUseFailure がやってくれる: 失敗のロギング、後始末スクリプトの起動、Claudeに additionalContext で「次こうしてね」のヒントを渡す
  • やってくれない: PostToolUseFailure で失敗そのものを取り消すことはできない、PermissionRequest で exit 2 による拒否はできない、自動モードのように人間ダイアログが出ない設定下では PermissionRequest 自体が発火しない場面もある
  • 意味が薄い場面: 1人で書き捨てのプロジェクトで全部 yolo モードで動かしている時。最初から人間確認をスキップしている運用ではゲート役のフックを書いても発火機会がない

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

シナリオ1: 料理ブログのデプロイ自動化で「rm だけ手動・push は自動」を分けたいとき

家庭料理のレシピを毎週載せるブログを Claude Code で更新運用しているとします。Claudeに「画像を WebP に変換して、不要なPNGは削除、それから git push まで」と頼みたい。ただ rm 系だけは怖いので確認したい。PermissionRequest で「rm から始まるコマンドは deny、git push origin maincwebp は allow」と書けば、Claudeの作業が止まらず、消す瞬間だけ手元に確認が回ってきます。permissionRules を返せば次回以降は git push でいちいち聞かれなくなる。

シナリオ2: 家計簿アプリの開発で「テスト失敗を見逃したくない」とき

夜中にClaudeに「家計簿の集計バグを直して、テストも追加して」と頼んで自分は寝るとする。朝起きて「全部終わってます」と言われるけど、実は途中で npm test が3回失敗していて、Claudeが勝手にテストを書き換えて通る形にしていた、みたいな事故が起きうる。PostToolUseFailurenpm test の失敗を全部 Slack に流しておけば、朝Slackを見るだけで「何回失敗したか・どんなエラーだったか」が時系列で並んでいる。Claudeの「終わりました」報告だけを信じずに済みます。

シナリオ3: 仕事の業務スクリプトで「危ない宛先のメール送信だけ止めたい」とき

顧客リストにメールを一斉送信する社内スクリプトをClaudeに叩かせるとします。テスト用の --dry-run 付きは黙って通したい、本番送信の --production 付きは絶対に人間確認を入れたい。PermissionRequesttool_input.command を見て、--production が入っている場合は deny--dry-run なら allow。さらに updatedInput で「コマンドの末尾に --limit 1 を強制追加して許可」というやり方もできる。送信するけど1件だけ、という形に勝手に絞り込んで通す運用です。

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

  • PermissionRequest で exit code 2 を返すと拒否は成立するが説明が一切残らない。deny 自体は効く。ただし message・updatedInput・permissionRules といった「許可方の細かい制御」は全部JSONじゃないと渡せない。実用上は JSON で hookSpecificOutput.decision.behavior: "deny" を返して exit 0、が事故りにくい書き方
  • PostToolUseFailure は失敗を取り消せない。「テストが失敗したから直前のコマンドを再実行」みたいな処理を書こうとしても、ツール側はもう失敗確定。やれるのは additionalContext でClaudeに次の手のヒントを渡すところまで
  • decision オブジェクトが hookSpecificOutput の中にネストされている。トップレベルに "decision" を書いてしまうと PermissionRequest では効かない。hookSpecificOutput.decision.behavior の形が正しい
  • updatedInput はツール毎にフィールド構造が違う。Bash なら {"command": "..."}、Write なら {"file_path": "...", "content": "..."}。書き換えたいツールの tool_input 構造を必ず合わせる。形が違うとClaudeが入力エラーで止まる
  • permissionRules は強力すぎる。一度 "Bash(git *)" みたいな広いルールを書き込むと、以降そのプロジェクトで git 系コマンドが全部素通りになる。狭い pattern を心がけて、"Bash(npm test)" のように特定コマンドに絞るのが安全
  • PermissionRequest と PermissionDenied は別系統。前者は「人間ダイアログの直前」、後者は「自動モードで分類器が拒否した後」。混同して同じ目的に両方書くと挙動が読めなくなる
  • フックスクリプトの実行権限を忘れるchmod +x .claude/hooks/*.sh しておかないと、JSONを返す前にフック自体が起動失敗で stderr だけ出して終わる
  • jq がMacに標準で入っていない。スクリプトの中で jq を使うなら先に brew install jq。フックは静かに失敗するので「動いてない理由」が分かりにくい

書き方

.claude/settings.json の hooks ブロックに、PostToolUseFailure / PermissionRequest をイベント名として書く:

{
  "hooks": {
    "PermissionRequest": [
      { "matcher": "Bash", "hooks": [{ "type": "command", "command": ".claude/hooks/bash-gate.sh" }] }
    ],
    "PostToolUseFailure": [
      { "matcher": "Bash", "hooks": [{ "type": "command", "command": ".claude/hooks/notify-test-failure.sh" }] }
    ]
  }
}

スクリプト側は標準入力でJSONを受け取り、標準出力にJSONを返す。

やってみるとこうなる

入力

PermissionRequest の入力例(Claude が npm test を叩こうとした瞬間):

{
  "session_id": "abc123",
  "hook_event_name": "PermissionRequest",
  "tool_name": "Bash",
  "tool_input": { "command": "npm test" },
  "tool_use_id": "tooluse_123abc",
  "permission_mode": "default"
}

PostToolUseFailure の入力例(npm test が exit 1 で落ちた直後):

{
  "session_id": "abc123",
  "hook_event_name": "PostToolUseFailure",
  "tool_name": "Bash",
  "tool_input": { "command": "npm test" },
  "tool_use_id": "tooluse_123abc",
  "permission_mode": "default",
  "effort": { "level": "medium" },
  "error": "Command failed with exit code 1",
  "stderr": "npm ERR! Test suite: 3 failed, 12 passed"
}

両者は同じ tool_use_id を持つので、1回のツール呼び出しを最初から最後まで突き合わせて追える。

出力例

PermissionRequest で「許可」を返す:

{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": { "behavior": "allow" }
  }
}

PermissionRequest で「拒否+メッセージ」:

{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": {
      "behavior": "deny",
      "message": "rm -rf は自動承認しない。手動で打ち直してください"
    }
  }
}

PermissionRequest で「コマンドを書き換えて許可+以降の自動許可ルール追加」:

{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": {
      "behavior": "allow",
      "updatedInput": { "command": "npm test -- --bail" },
      "permissionRules": [{ "tool": "Bash", "pattern": "npm test*" }]
    }
  }
}

PostToolUseFailure で Claude に補足情報を渡す:

{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUseFailure",
    "additionalContext": "テストが失敗しました。失敗ログは Slack に通知済み。該当テストファイルを開いて原因を特定してください。"
  }
}

注意: PermissionRequest は exit code 2 でも拒否扱いにはなるが、message・updatedInput・permissionRules を渡したいなら必ず JSON で decision.behavior を返して exit 0。実用上はJSON経由が定石。

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

フック
Claude Codeが何かする瞬間(ツール実行前、実行後、人間に質問する直前など)に割り込んで、自分のスクリプトを動かせる仕組み
tool_use_id
1回のツール呼び出しに振られる識別ID。PreToolUse / PermissionRequest / PostToolUse / PostToolUseFailure の全イベントで同じ値が流れてくるので、1個のツール呼び出しを最初から最後まで突き合わせて追える
hookSpecificOutput
フックがJSONで返す結果をまとめるブロック。PermissionRequestなら decision.behavior(allow / deny)、PostToolUseFailureなら additionalContext(Claudeへの補足)などをここに入れる
decision.behavior
PermissionRequestで自動許可・拒否を決める値。<code>"allow"</code> なら人間ダイアログを出さずに即実行へ、<code>"deny"</code> なら拒否してメッセージを返す
updatedInput
PermissionRequest許可時に、Claudeが渡してきたコマンド内容を上書きできる仕掛け。「<code>rm -rf ./</code> を <code>rm -rf ./tmp</code> に絞り込んで許可」のような sanitize(安全化)に使う
permissionRules
PermissionRequestで「今後このパターンのコマンドは自動で許可」と書き足せるルール配列。settings.jsonのpermissionsと同じ形式で、1回許可すれば以降は同じパターンを聞かれなくなる
additionalContext
PostToolUseFailureが返せる文字列。Claudeの次のターンに補足情報として差し込まれて、Claudeが次の手を組み立てる材料になる
matcher
フックを発火させるツールの名前条件。<code>"Bash"</code> ならBashツールのみ、<code>"Edit|Write"</code> なら EditとWrite両方、<code>"mcp__.*"</code> なら全MCPツールが対象になる正規表現
exit code 2
スクリプトの終了コード2。Claude Codeのフックでは「ブロック扱い」として使われる慣習がある。PermissionRequestでも exit 2 は拒否扱いになるが、message・updatedInput・permissionRules を渡したいなら JSON で返す必要がある
permission_mode
Claude Codeが今どの権限モードで動いているかを示す値。<code>"default"</code> は通常モードで、危ないツール呼び出しは人間ダイアログで聞かれる

関連項目

公式ドキュメント

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

-

← 戻る