Ein Plattformteam ermöglicht Claude Code in seiner gesamten Entwicklungsorganisation. Um Richtlinien für Toolaufrufe durchzusetzen, richten sie einen Hook-Server ein – einen lokalen Prozess, der Hook-Ereignisse auf stdin empfängt und eine JSON-Entscheidung auf stdout zurückgibt. Die erste Woche sieht gut aus. PreToolUse wird vor jedem Tool-Aufruf ausgelöst, der Hook gibt deny für alles zurück, was die Produktion berührt, und das Team ist davon überzeugt, dass die Durchsetzung funktioniert.
Dann liefert Claude Code eine neue Version aus. PermissionRequest beginnt neben PreToolUse zu schießen. Der Hook-Server gibt denselben permissionDecision JSON zurück, den er bereits zurückgegeben hat. Claude Code akzeptiert die Antwort, analysiert sie, findet kein Feld, das es für dieses Ereignis erkennt, und fährt fort, als ob keine Entscheidung getroffen worden wäre. Das Dementi wird stillschweigend ignoriert. Der Durchsetzungspunkt des Teams ist jetzt eine Wand mit einer Lücke darin, und in den Protokollen steht nichts darüber.
Dies ist das Problem, das ein verwalteter PEP lösen muss: Der Hook-Lebenszyklus von Claude Code ist kein einzelnes Ereignis mit einem Wire-Format. Es handelt sich um etwa 30 Ereignisse, jedes mit einem anderen Ausgabeschema, das von der Laufzeit berücksichtigt wird, und ein Fehler im Schema ist nicht von Stille zu unterscheiden.
Der Drahtmechanismus ist der Sicherheitsvertrag
Der Grund dafür, dass ein einzelnes permissionDecision-Feld nicht überall funktioniert, liegt darin, dass sich die Hook-Ereignisse von Claude Code unabhängig voneinander entwickelt haben. Die Ausgabeform, die einen Werkzeugaufruf auslöst, ist nicht die Form, die eine Eingabeaufforderungsübermittlung auslöst, und sie ist auch nicht die Form, die eine Aufgabe stoppt.
Es gibt sechs verschiedene Drahtmechanismen:
| Mechanismus | Ausgabeform | Veranstaltungen |
|---|---|---|
permissionDecision | hookSpecificOutput.permissionDecision (allow/deny/ask/defer) | PreToolUse |
permissionBehavior | hookSpecificOutput.decision.behavior (allow/deny) | PermissionRequest |
topLevelDecision | Oberste Ebene decision: "block" + reason | UserPromptSubmit, UserPromptExpansion, PreCompact, ConfigChange, PostToolBatch |
continueFalse | continue: false + stopReason | TaskCreated, TaskCompleted, TeammateIdle |
postToolUse | decision: "block" (sperrt weitere Bearbeitung; Werkzeug bereits ausgeführt) | PostToolUse |
neutral | Keine durchsetzbare Sperre | Alle context/observe-Ereignisse sowie invertierte Ereignisse wie Stop |
Wenn ein PEP permissionDecision: "deny" bei einem PermissionRequest-Ereignis zurückgibt, ignoriert Claude Code es – dieses Ereignis erwartet decision.behavior, nicht permissionDecision. Der JSON ist gültig, die HTTP-Antwort ist 200 und die Ablehnung ist nicht vorhanden. Dies ist kein theoretischer Randfall; Es handelt sich um den dokumentierten Drahtvertrag, der anhand von code.claude.com/docs/en/hooks überprüft wurde.
Dreifache Klassifizierung: Gating, Kontext, Beobachtung
Nicht jedes Hook-Ereignis kann eine Aktion auslösen. Der Versuch, ein SessionEnd oder ein Notification abzulehnen, ist sinnlos – diese Ereignisse dienen der Information und unterliegen keiner Entscheidungskontrolle. Der Versuch, ein Stop-Ereignis abzulehnen, ist mehr als bedeutungslos: decision: "block" auf Stop hält den Agenten am Laufen, was das Gegenteil eines Sicherheitsstopps ist.
Der PEP klassifiziert jedes erkannte Hook-Ereignis in eine von drei Kategorien:
Gating-Ereignisse enthalten eine Entscheidung, die eine Aktion zulassen, verweigern oder blockieren kann. Sie sind die Durchsetzungsfläche. Aber nicht alle Gating-Ereignisse sind gleichermaßen durchsetzbar: Stop und SubagentStop werden in der eigenen Taxonomie von Claude Code als Gating klassifiziert, aber ihre Blocksemantik ist invertiert (Block = weiterlaufen), sodass der PEP sie als neutral behandelt – er gibt niemals einen Block aus, der einen Agent entgegen der Absicht des Operators am Leben halten würde. In ähnlicher Weise verfügen Elicitation und ElicitationResult über eine Gating-Klassifizierung, verfügen jedoch in der aktuellen Version nicht über einen Wired-Action-Mechanismus, sodass der PEP sie standardmäßig auf „Neutral“ setzt, anstatt so zu tun, als gäbe es eine Durchsetzung, wo dies nicht der Fall ist.
Kontextereignisse ermöglichen es dem PEP, additionalContext einzuschleusen oder die Ausgabe neu zu schreiben, können die Aktion jedoch nicht wirklich blockieren. PostToolUse ist die teilweise Ausnahme – es kann die weitere Verarbeitung einer markierten Ausgabe blockieren, das Tool wurde jedoch bereits ausgeführt. Der Rest (PermissionDenied, MessageDisplay, SessionStart, Setup, SubagentStart, PostCompact, InstructionsLoaded, PostToolUseFailure) dient der Beobachtung mit Kontext: nützlich zum Anreichern der Modellansicht, nicht zum Stoppen.
Beobachten-Ereignisse (Notification, SessionEnd, StopFailure, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove) haben überhaupt keine Entscheidungskontrolle. Der PEP erfasst sie für den Telemetriepfad und das SIEM-Inventar, antwortet neutral und geht weiter.
Diese Klassifizierung ist nicht beratend. Es bestimmt, ob der Composition-Root-Entscheider eine Richtlinienregel anwendet (gating + erzwingbar), Kontext einfügt (context) oder nur beobachtet (observe). Etwas falsch zu machen bedeutet entweder eine falsche Durchsetzung (Rückgabe einer Ablehnung, die ignoriert wird) oder eine verpasste Durchsetzung (ein Gating-Ereignis wird als Beobachtung behandelt).
Die HookSpecs-Karte: Single Source of Truth
Die Klassifizierung, der Drahtmechanismus und die Durchsetzbarkeitsflagge sind in einer Karte zusammengefasst. Dies ist der tatsächliche Code, den der Connector verwendet – sowohl der HTTP-Antwort-Renderer als auch der Composition-Root-Entscheider lesen daraus:
type hookSpec struct {
class string // "gating" | "context" | "observe"
mech hookMech
enforceable bool
}
var hookSpecs = map[string]hookSpec{
// GATING — eine Hook-Rückgabe kann allow/deny/block auf die Aktion anwenden.
"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}, // invertiert
"SubagentStop": {"gating", mechNeutral, false}, // invertiert
"Elicitation": {"gating", mechNeutral, false}, // in v1 nicht verdrahtet
"ElicitationResult": {"gating", mechNeutral, false},
// CONTEXT — additionalContext / Ausgabeumschreibung, keine echte Blockierung.
"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 — keine Entscheidungskontrolle.
"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},
}
Dreißig Ereignisse mit jeweils genau einer Klassifizierung, einem Drahtmechanismus und einem Vollstreckbarkeitsurteil. Der Renderer konsultiert hookMechFor(event), um zu entscheiden, welche JSON-Form ausgegeben werden soll. Der Entscheider konsultiert HookEnforcementFor(event), um zu entscheiden, ob eine Richtlinienregel angewendet, Kontext eingefügt oder beobachtet werden soll. Beide lesen von derselben Karte und können daher nicht widersprechen.
Der Standardwert „Deny-Closed“.
Die wichtigste Funktion in der Datei ist vier Zeilen lang:
func hookSpecFor(event string) hookSpec {
if s, ok := hookSpecs[event]; ok {
return s
}
return hookSpec{class: "unknown", mech: mechPermissionDecision, enforceable: true}
}
Ein Ereignis, das nicht in der Karte enthalten ist – weil Claude Code ein neues Hook-Ereignis geliefert hat, das der Connector noch nicht klassifiziert hat – wird als unknown behandelt, dem mechPermissionDecision-Drahtmechanismus zugewiesen und als durchsetzbar markiert. Dies ist die Standardeinstellung „Deny-Closed“: Ein nicht erkanntes Ereignis ist ein Berechtigungstor, kein stiller Pass-Through. Wenn keine Richtlinienregel übereinstimmt, gilt die konfigurierte Standardeinstellung des Betreibers, die in einer kontrollierten Bereitstellung „Verweigern“ lautet.
Die Alternative – die Standardeinstellung „Neutral“ oder „Beobachten“ – würde bedeuten, dass jedes neue Hook-Ereignis, das Claude Code einführt, unkontrolliert ist, bis es jemand bemerkt und zur Karte hinzufügt. In einem Deny-Closed-Modell wird das neue Ereignis ab dem Moment seiner Auslösung gesteuert, auch wenn die Klassifizierung konservativ ist. Eine falsche Ablehnung eines neuen Ereignisses ist sichtbar und korrigierbar; Eine stille Erlaubnis ist unsichtbar und kann monatelang bestehen bleiben.
Die Struktur HookEnforcement exportiert diese Klassifizierung, sodass der Entscheider drei Ebenen von Gating-Ereignissen unterscheiden kann:
- Klassisches Gate (
PreToolUse,PermissionRequest,PostToolUseund jedes unbekannte Ereignis): Wenn keine geregelte Regel übereinstimmt, gilt der Richtlinienstandard des Betreibers (verweigern-geschlossen). - Lebenszyklus-Gate (andere erzwingbare Gating-Ereignisse wie
UserPromptSubmit,TaskCreated): Der Entscheider wendet stattdessen einen sicheren Standard pro Ereignis an – neutral für UX/lifecycle-Ereignisse, verweigert für Zustandsmutationsereignisse. - Nicht erzwingbares Tor (
Stop,SubagentStop,Elicitation): Der Entscheider gibt trotzdem neutral zurück. Das Aussenden einer Ablehnung, die Claude Code als „Weiterlaufen“ interpretiert, wäre das Gegenteil von sicher.
Verteilung: von der Klassifizierung bis zur Flotte
Ereignisse richtig zu klassifizieren ist die eine Hälfte. Die andere Möglichkeit besteht darin, den PEP auf jede Claude Code-Instanz zu übertragen. Der Connector für verwaltete Einstellungen rendert die Hook-Konfiguration in die von Claude Code erwartete Form und verteilt sie als vom Server verwaltete Einstellungsdatei – eine nicht überschreibbare Konfiguration, die die Steuerungsebene an jeden verwalteten Host weiterleitet.
Der PEP-Hook wird mit einem leeren Matcher verteilt (entspricht allen Tools – kein Tool entgeht dem Durchsetzungspunkt) und mit allowManagedHooksOnly gepaart, um zu verhindern, dass die lokalen Hooks eines Entwicklers den verwalteten PEP unterbieten. Ohne dieses Flag könnte ein Hook auf Benutzerebene den verwalteten Hook überschatten, und die Durchsetzung würde scheinbar vorhanden sein, wäre aber umgehbar. Die Validierung zur Erstellungszeit erkennt Folgendes: Wenn eine Richtlinie einen PreToolUse PEP-Hook ohne allowManagedHooksOnly ausliefert, gibt die Konsole eine Manipulationsschutzwarnung aus.
Der Telemetriepfad ist separat und planunabhängig: Der OpenTelemetry-Export von Claude Code wird über verwaltete Umgebungsvariablen (CLAUDE_CODE_ENABLE_TELEMETRY, die OTEL_*-Exportschlüssel) aktiviert, sodass die Steuerungsebene die Abonnementnutzung beobachten kann, ohne Proxy-Inferenz oder die Abonnement-Anmeldeinformationen zu berühren. Die Inhaltserfassung (OTEL_LOG_USER_PROMPTS, OTEL_LOG_TOOL_CONTENT) ist standardmäßig deaktiviert. Das Einschalten ist eine bewusste, markierte Entscheidung, da es Eingabeaufforderungs- und Toolinhalte vom Computer des Entwicklers verschickt und so eine Residenz- und Redaktionspflicht schafft, die die Steuerungsebene besitzen muss.
Was dies für eine kontrollierte Bereitstellung bedeutet
Ein Team, das Claude Code mit einem verwalteten PEP ausführt, erhält einige wichtige Eigenschaften:
- Keine stille Zulassung. Jedes Hook-Ereignis wird klassifiziert und jedes nicht erkannte Ereignis wird standardmäßig abgelehnt. Eine neue Claude Code-Version kann kein unkontrolliertes Lebenszyklusereignis einführen.
- Korrekte Drahtform pro Ereignis. Das PEP gibt kein generisches JSON-Objekt zurück und hofft, dass Claude Code es berücksichtigt. Es gibt das genaue Ausgabeschema zurück, das das jeweilige Ereignis erwartet, da eine Ablehnung im falschen Schema keine Ablehnung ist.
- Ehrliche Nichtdurchsetzung. Ereignisse, die nicht durchgesetzt werden können (Beobachtungsereignisse, invertierte Gating-Ereignisse), werden nicht vorgetäuscht. Der PEP zeichnet sie auf, liefert eine neutrale Antwort und vermittelt dem Bediener kein falsches Gefühl der Kontrolle.
- Manipulationsschutz bei der Verteilung. Die Ebene der verwalteten Einstellungen stellt sicher, dass der PEP-Hook nicht überschreibbar ist, und markiert Konfigurationen, bei denen er unterschritten werden könnte.
Das PEP ist die Durchsetzungshälfte eines größeren, geregelten Betriebsmodells. Die Zugriffszuordnung, das Audit-Ledger und die Policy-as-Code-Schicht um sie herum werden in der Produktübersicht und der Hooks-Dokumentation behandelt. Wenn Sie sehen möchten, wie sich der Telemetriepfad und das Berechtigungs-Gate zusammensetzen, geht die Architekturübersicht durch beide.