プラットフォーム チームは、エンジニアリング組織全体で Claude Code を有効にします。ツール呼び出しにポリシーを適用するために、フック サーバー (標準入力でフック イベントを受信し、標準出力で JSON 決定を返すローカル プロセス) を接続します。最初の1週間は大丈夫なようです。 PreToolUse はすべてのツール呼び出しの前に起動され、本番環境に関わるものに対してフックは deny を返します。チームは強制が機能していると信じています。
その後、Claude Code が新しいリリースを出荷します。 PermissionRequest は PreToolUse と一緒に発射を開始します。フック サーバーは、すでに返していたのと同じ permissionDecision JSON を返します。 Claude Code は応答を受け入れ、解析し、そのイベントに対して認識できるフィールドを見つけず、決定が与えられなかったかのように続行します。拒否は黙って無視されます。チームの執行ポイントは現在、gap が含まれる壁になっていますが、ログにはそのようなことは何も記載されていません。
これは、ガバナンスされた PEP が解決しなければならない問題です。Claude Code のフック ライフサイクルは、1 つのワイヤー形式を持つ 1 つのイベントではありません。これは approximately 30 個のイベントであり、それぞれランタイムが優先する異なる出力スキーマを持ち、スキーマ内の間違いは沈黙と区別できません。
ワイヤーメカニズムはセキュリティ契約です
単一の permissionDecision フィールドがどこでも機能しない理由は、Claude Code のフック イベントが独立して進化したためです。ツール呼び出しをゲートする出力シェイプは、プロンプト送信をゲートするシェイプではなく、タスクを停止するシェイプでもありません。
6 つの異なるワイヤー メカニズムがあります。
| 仕組み | 出力形状 | イベント |
|---|---|---|
permissionDecision | hookSpecificOutput.permissionDecision (/deny/ask/defer を許可) | プレツールの使用 |
permissionBehavior | hookSpecificOutput.decision.behavior (/deny を許可) | 許可リクエスト |
topLevelDecision | トップレベル decision: "block" + reason | UserPromptSubmit、UserPromptExpansion、PreCompact、ConfigChange、PostToolBatch |
continueFalse | continue: false + stopReason | TaskCreated、TaskCompleted、TeammateIdle |
postToolUse | decision: "block" (さらなる処理をブロックします。ツールはすでに実行されています) | ポストツール使用 |
neutral | 強制可能なブロックはありません | すべてのコンテキスト/observeイベント、およびStopなどの反転イベント |
PEP が PermissionRequest イベントで permissionDecision: "deny" を返した場合、Claude Code はそれを無視します。そのイベントは permissionDecision ではなく、decision.behavior を予期します。 JSON は有効で、HTTP 応答は 200 で、拒否は存在しません。これは理論上の特殊なケースではありません。これは文書化された電信契約であり、code.claude.com/docs/en/hooks に対して検証されています。
3 方向の分類: ゲート、コンテキスト、観察
すべてのフック イベントがアクションをゲートできるわけではありません。 SessionEnd または Notification を拒否しようとしても無意味です。これらのイベントは情報提供であり、意思決定を制御するものではありません。 Stop イベントを拒否しようとすることは、無意味であるよりも悪質です。Stop 上の decision: "block" は、エージェントを実行し続けます。これは、安全停止の逆です。
PEP は、認識されたすべてのフック イベントを次の 3 つのカテゴリのいずれかに分類します。
ゲーティング イベントには、アクションを許可、拒否、ブロックする決定が含まれます。それらは執行面です。しかし、すべてのゲーティング イベントが同様に強制できるわけではありません。Stop と SubagentStop は、Claude Code 独自の分類法ではゲーティングとして分類されますが、それらのブロック セマンティクスは反転されている (ブロック = 実行を継続する) ため、PEP はこれらを中立として扱います。オペレーターの意図に反してエージェントを存続させるブロックを発行することはありません。同様に、Elicitation と ElicitationResult にはゲート分類がありますが、現在のバージョンでは有線アクション メカニズムが欠如しているため、PEP は、強制が存在しない場合に適用が存在するかのように振る舞うのではなく、デフォルトでニュートラルに設定します。
コンテキスト イベントにより、PEP は additionalContext を挿入したり、出力を書き換えたりできますが、アクションを完全にブロックすることはできません。 PostToolUse は partial 例外です。フラグが設定された出力のさらなる処理をブロックできますが、ツールはすでに実行されています。残り (PermissionDenied、MessageDisplay、SessionStart、Setup、SubagentStart、PostCompact、InstructionsLoaded、PostToolUseFailure) はコンテキスト付き観察です。モデルのビューを停止するのではなく、モデルのビューを強化するのに役立ちます。
観察 イベント (Notification、SessionEnd、StopFailure、CwdChanged、FileChanged、WorktreeCreate、WorktreeRemove) には、決定制御がまったくありません。 PEP はそれらをテレメトリ パスと SIEM インベントリに記録し、中立的に応答し、次に進みます。
この分類は勧告ではありません。これは、構成ルート ディサイダーがポリシー ルールを適用するか (ゲート + 強制可能)、コンテキストを挿入するか (コンテキスト)、または単に観察するか (観察) を決定します。間違った解釈をすると、誤った強制 (無視される拒否を返す) または強制の欠如 (ゲート イベントを観察として扱う) のいずれかを意味します。
HookSpecs マップ: 唯一の信頼できる情報源
分類、ワイヤーメカニズム、強制可能フラグは 1 つのマップ内に存在します。これは、コネクタが使用する実際のコードです。HTTP 応答レンダラーと構成ルート ディサイダーの両方がそこから読み取ります。
type hookSpec struct {
class string // "gating" | "context" | "observe"
mech hookMech
enforceable bool
}
var hookSpecs = map[string]hookSpec{
// GATING — フックの戻り値でアクションを allow/deny/block できる。
"PreToolUse": {"gating", mechPermissionDecision, true},
"PermissionRequest": {"gating", mechPermissionBehavior, true},
"UserPromptSubmit": {"gating", mechTopLevelDecision, true},
"UserPromptExpansion": {"gating", mechTopLevelDecision, true},
"PreCompact": {"gating", mechTopLevelDecision, true},
"ConfigChange": {"gating", mechTopLevelDecision, true},
"PostToolBatch": {"gating", mechTopLevelDecision, true},
"TaskCreated": {"gating", mechContinueFalse, true},
"TaskCompleted": {"gating", mechContinueFalse, true},
"TeammateIdle": {"gating", mechContinueFalse, true},
"Stop": {"gating", mechNeutral, false}, // 反転
"SubagentStop": {"gating", mechNeutral, false}, // 反転
"Elicitation": {"gating", mechNeutral, false}, // v1 では未接続
"ElicitationResult": {"gating", mechNeutral, false},
// CONTEXT — additionalContext / 出力の書き換え、実際のブロックはなし。
"PostToolUse": {"context", mechPostToolUse, true},
"PostToolUseFailure": {"context", mechNeutral, false},
"PermissionDenied": {"context", mechNeutral, false},
"MessageDisplay": {"context", mechNeutral, false},
"SessionStart": {"context", mechNeutral, false},
"Setup": {"context", mechNeutral, false},
"SubagentStart": {"context", mechNeutral, false},
"PostCompact": {"context", mechNeutral, false},
"InstructionsLoaded": {"context", mechNeutral, false},
// OBSERVE — 意思決定の制御はなし。
"Notification": {"observe", mechNeutral, false},
"SessionEnd": {"observe", mechNeutral, false},
"StopFailure": {"observe", mechNeutral, false},
"CwdChanged": {"observe", mechNeutral, false},
"FileChanged": {"observe", mechNeutral, false},
"WorktreeCreate": {"observe", mechNeutral, false},
"WorktreeRemove": {"observe", mechNeutral, false},
}
30 件のイベント。それぞれに 1 つの分類、1 つのワイヤー メカニズム、および 1 つの強制可能性の判定が含まれます。レンダラーは hookMechFor(event) を参照して、どの JSON 形状を出力するかを決定します。ディサイダーは HookEnforcementFor(event) を参照して、ポリシー ルールを適用するか、コンテキストを挿入するか、監視するかを決定します。どちらも同じ地図から読み取っているので、意見が異なるはずはありません。
拒否クローズのデフォルト
ファイル内の最も重要な関数は 4 行の長さです。
func hookSpecFor(event string) hookSpec {
if s, ok := hookSpecs[event]; ok {
return s
}
return hookSpec{class: "unknown", mech: mechPermissionDecision, enforceable: true}
}
マップにないイベント (Claude Code がコネクタがまだ分類していない新しいフック イベントを出荷したため) は、unknown として扱われ、mechPermissionDecision ワイヤー メカニズムが割り当てられ、強制可能としてマークされます。これは拒否クローズのデフォルトです。認識されないイベントは許可ゲートであり、サイレント パススルーではありません。一致するポリシー ルールがない場合は、オペレータが設定したデフォルト ポスチャが適用されますが、管理された展開ではこれは拒否されます。
別の方法 (デフォルトでは中立または監視) では、Claude Code が導入するすべての新しいフック イベントは、誰かが気づいてマップに追加するまで制御されないことを意味します。 Deny-Closed モデルでは、たとえ分類が保守的であっても、新しいイベントは発生した瞬間から制御されます。新しいイベントに対する誤った拒否は表示され、修正可能です。サイレント許可は目に見えず、数か月間持続する場合があります。
HookEnforcement 構造体はこの分類をエクスポートするため、ディサイダーは 3 つの層のゲート イベントを区別できます。
- クラシック ゲート (
PreToolUse、PermissionRequest、PostToolUse、および不明なイベント): 一致する管理ルールがない場合、オペレーターのポリシーのデフォルトが適用されます (拒否クローズ)。 - ライフサイクル ゲート (
UserPromptSubmit、TaskCreatedなどの他の強制可能なゲート イベント): ディサイダーは代わりにイベントごとの安全なデフォルトを適用します。UX/lifecycle イベントには中立、状態変異イベントには拒否です。 - 強制不可能なゲート (
Stop、SubagentStop、Elicitation): 決定者は関係なく中立を返します。 Claude Code が「実行を続ける」と解釈するような拒否を発行することは、安全とは逆になります。
分布: 分類からフリートまで
イベントを正しく分類できれば半分です。もう 1 つは、すべての Claude Code インスタンスに PEP を取得することです。管理設定コネクタは、フック構成を Claude Code が期待する形状にレンダリングし、サーバー管理設定ファイル (コントロール プレーンがすべての管理対象ホストにプッシュするオーバーライド不可能な構成) として配布します。
PEP フックは空のマッチャー (すべてのツールに一致します。強制ポイントを回避するツールはありません) とともに配布され、開発者のローカル フックがマネージド PEP を侵害するのを防ぐために allowManagedHooksOnly と組み合わせます。このフラグがないと、ユーザーレベルのフックがマネージドフックをシャドウイングする可能性があり、強制はバイパス可能であるにもかかわらず存在しているように見えます。オーサリング時の検証はこれを検出します。ポリシーが allowManagedHooksOnly なしで PreToolUse PEP フックを出荷する場合、コンソールは改ざん防止アドバイザリを生成します。
テレメトリ パスは別個であり、プラン非ゲートです。Claude Code の OpenTelemetry エクスポートは、管理された環境変数 (CLAUDE_CODE_ENABLE_TELEMETRY、OTEL_* エクスポーター キー) を介して有効になるため、コントロール プレーンは推論をプロキシしたりサブスクリプション資格情報に触れたりすることなく、サブスクリプションの使用を監視できます。コンテンツ キャプチャ (OTEL_LOG_USER_PROMPTS、OTEL_LOG_TOOL_CONTENT) はデフォルトでオフになっています。これをオンにすることは、プロンプトとツールのコンテンツを開発者のマシンから出荷し、コントロール プレーンが所有する必要がある常駐と編集義務を作成するため、意図的なフラグ付きの選択です。
これが管理された展開にとって何を意味するか
管理された PEP を使用して Claude Code を実行しているチームは、重要なプロパティをいくつか取得します。
- サイレント許可はありません。 すべてのフック イベントは分類され、すべての認識されないイベントはデフォルトで拒否されます。新しい Claude Code リリースでは、管理されていないライフサイクル イベントを導入することはできません。
- イベントごとの正しいワイヤー形状。 PEP は汎用の JSON オブジェクトを返さないので、Claude Code がそれを尊重することを望みます。間違ったスキーマでの拒否は拒否ではないため、特定のイベントが予期する正確な出力スキーマを返します。
- 正直な非強制。 強制できないイベント (監視イベント、反転ゲート イベント) は、強制されたふりをしません。 PEP はそれらを記録し、中立を返し、オペレーターに誤った制御感を与えません。
- 配布時の改ざん防止 管理設定レイヤーは、PEP フックがオーバーライド不可能であることを保証し、アンダーカットされる可能性のある構成にフラグを立てます。
PEP は、より大規模な管理された運用モデルの施行の半分です。アクセス マップ、監査台帳、およびその周りのコードとしてのポリシー層については、製品概要 および フック ドキュメント で説明されています。テレメトリ パスと許可ゲートがどのように構成されているかを確認したい場合は、アーキテクチャの概要 で両方を詳しく説明します。