Claude Code safety_event — 拒否と承認待ちをcategory別に集計する
claude_code.safety_eventのcategory・decision・reason_code・circuit_breakerの読み方と、tool_use_idでtool_decisionにつないで拒否の中身を特定します。
Claude Codeがツール呼び出しを止めた理由を後から集計したいなら、見るのはclaude_code.safety_eventです。権限ルール、フック、auto modeの分類器、サンドボックスのどれが止めたのかが、categoryという1つの属性に入ります。
ただし、このイベントが持つ属性にはコマンド文字列もファイルパスも入りません。「何が止まったか」はtool_decisionと突き合わせて読みます。この記事は、属性の意味、集計の切り口、突き合わせの手順を順に並べます。
safety_eventはどんなときに出るのか
イベントは、チェックがツール呼び出しを拒否したとき、または承認プロンプトを要求したときに出ます。これに加えて次の2つでも出ます。
- サンドボックスのネットワークプロキシが接続やリクエストを拒否したとき
- フックがツールの結果、またはターンの終了を差し戻したとき
Claude Code v2.1.307以降が必要です。専用の有効化スイッチはなく、他のイベントと同じ経路で出力されます。つまりCLAUDE_CODE_ENABLE_TELEMETRY=1とログのエクスポーターを設定していれば、追加の設定は要りません。
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# コマンドやMCPツール名まで追いたいときだけ付ける
export OTEL_LOG_TOOL_DETAILS=1
claudeOTEL_LOG_TOOL_DETAILS=1はプライバシーに関わる設定です。Bashのコマンド、MCPのサーバー名とツール名、スキル名などがイベントに載るようになるため、集計だけが目的なら最初は外しておく選択肢があります。
safety_eventが出ない拒否がある
すべての拒否にsafety_eventが付くわけではありません。次の呼び出しには、tool_decisionだけが残ります。
- カテゴリのないプロンプトで承認された呼び出し(
defaultモードで作業ディレクトリ内の編集を承認した場合など) canUseToolコールバックや--permission-prompt-toolのツールが、フックだけが要求したプロンプトで承認した呼び出しAskUserQuestionとExitPlanModeの呼び出し(辞退したかどうかを問わない)
もう1つ、-pで実行し、--permission-prompt-toolも--permission-prompts noneも付けていない場合です。カテゴリのないプロンプトに答える相手がいないため呼び出しが拒否され、safety_eventは出ません。このときはtool_decisionでdecisionがreject、sourceがconfigのイベントを探し、同じtool_use_idのsafety_eventが無いものを数えます。
つまり、safety_eventの件数だけを見ると、無人実行で黙って落ちた呼び出しを取りこぼします。
categoryの14値で「どの仕組みが止めたか」を分ける
集計の主軸はcategoryです。値ごとに、見るべき詳細属性が決まっています。
| category | 止めた仕組み | 続けて見る属性 |
|---|---|---|
permission_rule | 止めた仕組み管理設定以外のdenyまたはaskルール | 続けて見る属性rule_source |
managed_policy | 止めた仕組み管理設定のdenyまたはaskルール | 続けて見る属性rule_source |
permission_mode | 止めた仕組みdontAsk・planモード、またはプロンプトを出せないセッション | 続けて見る属性reason_code、reason_type |
hook | 止めた仕組みPreToolUseやPermissionRequestのフック | 続けて見る属性hook_event、reason_code |
auto_mode_block | 止めた仕組みauto modeの分類器によるブロック | 続けて見る属性classifier_category |
auto_mode_no_verdict | 止めた仕組み分類器の判定が得られなかった(リクエスト失敗など) | 続けて見る属性reason_code |
safety_check | 止めた仕組み作業ディレクトリ外のRead・Edit・Writeなどの組み込みチェック | 続けて見る属性circuit_breaker |
static_analysis | 止めた仕組み複雑すぎて解析できないBashやMonitorのコマンド | 続けて見る属性reason_code |
sandbox_override | 止めた仕組みサンドボックス外で実行し直すかのプロンプト | 続けて見る属性reason_code |
user_rejected | 止めた仕組みカテゴリのないプロンプトなどを人が拒否・却下した | 続けて見る属性decision_source |
sandbox_network | 止めた仕組み実行後、プロキシが接続やリクエストを拒否した | 続けて見る属性sandbox_violation_count |
sandbox_other | 止めた仕組みsandbox_networkと同様だが、拒否先がホスト名やIPとして読めない | 続けて見る属性sandbox_violation_count |
hook_review | 止めた仕組みフックが結果またはターン終了を差し戻した | 続けて見る属性hook_event |
other | 止めた仕組み上のどれにも当てはまらない拒否 | 続けて見る属性reason_code |
見落としやすいのは次の3点です。
- 管理設定のルールは
managed_policy、それ以外はpermission_ruleと分かれます。組織の方針による拒否と、個人のsettingsによる拒否は、この1属性で切れます。 hookは「呼び出しを拒否した」、hook_reviewは「実行後の結果やターン終了を差し戻した」です。後者のdecisionはrejectではなくsent_backになります。sandbox_networkとsandbox_otherは、呼び出し自体は実行されています。実行後に出るため、decisionだけで「呼び出しが止まった件数」を数えると意味が混ざります。
decisionは3値、decision_sourceは誰が決めたか
decisionはaccept、reject、sent_backの3値です。承認プロンプトが出て人が通したケースはacceptで記録されるので、「止められた」と「確認を求められた」を区別するにはcategoryとdecision_sourceを合わせて読みます。
decision_sourceはtool_decisionのsourceと同じ値(config、hook、user_temporary、user_permanent、user_abort、user_rejectなど)か、otherです。grant_scopeは、設定ファイルにルールを保存せずに承認したプロンプトについて、onceかsessionを持ちます。
1件のイベントを読む
ドキュメントには、管理設定のdenyルールがBashコマンドを拒否したときの属性が例として載っています。公式の例に沿った形にすると、次のとおりです。
{
"event.name": "safety_event",
"category": "managed_policy",
"decision": "reject",
"decision_source": "config",
"reason_code": "rule:policySettings",
"rule_source": "policySettings",
"tool_class": "bash",
"tool_name": "Bash",
"tool_source": "builtin",
"permission_mode": "default",
"tool_use_id": "toolu_01ABC123...",
"sandbox_violation_count": "0",
"sandbox_enabled": "false"
}読み取れるのは「管理設定のルールが、defaultモードのBashを、設定どおりに拒否した」という事実までです。どのコマンドかはここに無く、tool_use_idの先にあるtool_decisionで確かめます。tool_classはbash、file、web、mcpなどのまとまりで、ツールの種類別に件数を出したいときにtool_nameより安定した軸になります。
フックの差し戻しは2種類ある
hookはPreToolUseフックなどが呼び出しそのものを拒否した場合で、decisionはrejectです。hook_reviewは、呼び出しが実行された後にフックが結果を差し戻した場合と、ターンの終了を差し戻した場合で、decisionはsent_backになり、hook_eventがpost_tool_use、post_tool_use_failure、stop、subagent_stopのどれかを持ちます。
Stopフックで完了条件を強制している運用なら、hook_reviewかつhook_event=stopの件数が「完了と認められず差し戻された回数」として使えます。
reason_codeとcircuit_breakerの読み方
reason_codeは、そのチェックの理由を表す文字列です。ドキュメントに挙がっている値の例は次のとおりです。
| 例 | 読み方 |
|---|---|
rule:policySettings | 読み方管理設定のルールが理由 |
mode:dontAsk | 読み方dontAskモードが理由 |
hook:PreToolUse | 読み方PreToolUseフックが理由 |
classifier:auto-mode | 読み方auto modeの分類器が理由 |
bashMiss:too-complex | 読み方Bashコマンドが複雑すぎて解析できなかった |
unknown | 読み方理由を特定できなかった |
これらは「例」として載っている値で、全値の一覧ではありません。集計クエリを書くときは、reason_codeを固定の列挙として扱わず、categoryで先に絞ってからreason_codeの分布を見る形にしておくと、未知の値が増えても壊れにくくなります。
reason_typeはrule、subcommandResults、mode、hook、classifier、asyncAgentなどの分類です。rule_sourceはpolicySettings、userSettings、projectSettings、localSettings、flagSettings、cliArg、sessionのどの設定元のルールが関わったかを示します。
circuit_breakerは、categoryがsafety_checkのときの詳細で、dangerousRemoval、outsideReadsBlocked、claudeSettingsFileが例に挙がっています。
拒否の中身はtool_decisionにつないで読む
safety_eventの属性は、意図的にコマンドやパスを持ちません。ドキュメントは、MCPツールやコマンドを知りたいときはOTEL_LOG_TOOL_DETAILS=1にして、対応するtool_decisionのtool_parametersを読むよう案内しています。
つなぎ目はtool_use_idです。tool_decision、tool_result、safety_eventに共通して載ります。ただしターン終了を差し戻したsafety_eventには付きません。
手順は次の3段です。
- safety_eventを
categoryとdecisionで絞る - 件数が多いグループの
tool_use_idを取る - 同じ
tool_use_idのtool_decisionを引き、tool_parametersを読む
tool_parametersはJSON文字列で、Bashならbash_commandやfull_command、MCPならmcp_server_nameとmcp_tool_name、スキルならskill_nameを含みます。decisionがrejectのときに、どのコマンドが拒否されたかを知るための属性です。
ユーザー設定のMCPサーバーでは、tool_nameは常にmcp_toolという文字列になり、実際のサーバー名とツール名はtool_parameters側(フラグが有効なとき)にだけ出ます。MCP経由の拒否を名前別に集計したいなら、このフラグが前提になります。
auto modeの拒否は分類器の属性と合わせる
categoryがauto_mode_blockのとき、OTEL_LOG_TOOL_DETAILS=1ならclassifier_categoryにブロックの根拠となった組み込みルールの名前が入ります。例はdata_exfiltrationのように小文字とアンダースコアです。
tool_decision側のclassifier_verdictはallow、block、no_verdict、unavailableの4値です。no_verdictは「判定が得られなかったので拒否した」であり、分類器が危険と判断したわけではありません。ブロック件数に混ぜると、障害を危険な操作の増加と取り違えます。safety_eventではauto_mode_blockとauto_mode_no_verdictが別のカテゴリなので、分けて数えられます。
フックが入力を書き換えたことで拒否されるケースは、auto modeでa hook changed this call's inputと拒否される理由に切り分けがあります。
集計の切り口は3つあれば足りる
属性が多いので、最初から全部を使おうとせず、問いを3つに絞ると運用に乗ります。次はバックエンド非依存の擬似クエリです(特定製品の構文ではありません)。
-- 1. どの仕組みが何件止めたか
count(*) by category, decision
where event.name = "safety_event"
-- 2. 管理設定のルールで拒否された呼び出しの設定元
count(*) by rule_source, tool_name
where category = "managed_policy" and decision = "reject"
-- 3. サンドボックスのプロキシが拒否した通信がある呼び出し
count(*) by tool_use_id
where sandbox_violation_count is not null and sandbox_violation_count != "0"3番目は、sandbox_violation_countが文字列で入る点に注意が要ります。値は"0"から"20"の文字列なので、バックエンドで数値に変換されない場合は文字列として比較します。この属性はcategoryに関係なく、サンドボックス内のフォアグラウンドで終わったコマンドについて数えられます。
サンドボックスからの通信拒否の原因そのものは、sandboxから繋がらない原因と回避で扱っています。
件数が増えたときの見方
件数が急に増えたときは、次の順で当たりをつけます。
managed_policyが増えた: 管理設定のルールに当たる操作が増えた。rule_sourceとtool_nameで、どのツールが当たっているかを見るstatic_analysisが増えた: 複雑すぎるコマンドが増えた。reason_codeがbashMiss:too-complexかを確認するuser_rejectedが増えた: 人がプロンプトを断る頻度が上がった。decision_sourceで、却下か明示的な拒否かを分けるauto_mode_no_verdictが増えた: 分類器へのリクエスト自体が失敗している。ブロック増加と混同しない
どれも「危険な操作が増えた」とは限りません。権限ルールを厳しくした翌日にカテゴリが増えるのは、設定どおりに動いているだけです。
落とし穴
- 無人実行のgap:
-pで承認者がいない場合の拒否はsafety_eventに出ません。CIで使うならtool_decisionのrejectかつsource=configを別に数える - フラグの有無: safety_eventは
OTEL_LOG_TOOL_DETAILSを付けなくても出ます。フラグで変わるのはclassifier_categoryと、突き合わせ先のtool_parametersです - 値の確認:
reason_codeの値は例示です。アラート条件を作る前に、自分の環境で流れた実イベントでcategoryとreason_codeの組み合わせを確かめます
Claudeにクエリを書かせるときの渡し方
集計クエリをClaudeに書かせる場合は、この記事の属性表をそのまま貼り、使うバックエンドの構文を指定します。続けて、sandbox_violation_countが文字列であること、reason_codeは列挙として固定しないこと、tool_use_idが無いイベントがあることの3点を条件として添えます。これを省くと、数値比較や固定のIN句を含むクエリが返ってきやすくなります。
最初のクエリが返したら、実際のイベントを数件流してcategoryの値が想定どおりに入っているかを確かめてから、アラートに組み込みます。
周辺イベントとの役割分担
safety_eventは「ローカルの権限・安全チェック」の記録です。モデル側の拒否は別のイベントで、api_refusalイベントでの数え方が対になります。監査用に、どの問いをどのイベントで引くかを整理した表はセキュリティ監査とOTELイベントの対応表にあります。
ユーザーや組織の属性を付けて誰の操作かを追う設定はOTELユーザー特定の記事が扱い、SIEMへの送り方はSIEM連携の手順にまとまっています。safety_eventもこれらと同じ標準属性を持つため、ユーザー別・セッション別に切れます。
まとめ
safety_eventは「どの仕組みが止めたか」をcategoryで、「なぜか」をreason_codeやcircuit_breakerで答えるイベントです。「何を」止めたかは持たないので、tool_use_idでtool_decisionにつなぎ、OTEL_LOG_TOOL_DETAILS=1のときのtool_parametersで読みます。無人の-p実行の拒否とサンドボックス実行後のイベントは件数の意味がずれるため、tool_decisionとの併用が前提です。