Zum Inhalt springen

Claude Code

Innerhalb der Hooks PEP: Deny-Closed-Richtlinie innerhalb von Claude Code

Von Olivares AI 8 min Lesezeit

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:

MechanismusAusgabeformVeranstaltungen
permissionDecisionhookSpecificOutput.permissionDecision (allow/deny/ask/defer)PreToolUse
permissionBehaviorhookSpecificOutput.decision.behavior (allow/deny)PermissionRequest
topLevelDecisionOberste Ebene decision: "block" + reasonUserPromptSubmit, UserPromptExpansion, PreCompact, ConfigChange, PostToolBatch
continueFalsecontinue: false + stopReasonTaskCreated, TaskCompleted, TeammateIdle
postToolUsedecision: "block" (sperrt weitere Bearbeitung; Werkzeug bereits ausgeführt)PostToolUse
neutralKeine durchsetzbare SperreAlle 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.

Deny-Closed-Hook-PEP: Ereignisklassifizierung und Ablauf des Wire-Mechanismus

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, PostToolUse und 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Verwandte Beiträge

Häufige Fragen

Warum verweigert das PEP unbekannte Hook-Ereignisse standardmäßig, anstatt es zuzulassen?

Eine stille Zulassung für ein nicht erkanntes Ereignis bedeutet, dass jeder neue Hook Claude Code, der in einer zukünftigen Version ausgeliefert wird, die Durchsetzung umgeht, bis ihn jemand manuell zur Klassifizierungskarte hinzufügt. Das ist das Gegenteil der Sicherheitslage, die eine kontrollierte Bereitstellung erfordert. Die Standardeinstellung „Deny-Closed“ behandelt ein unbekanntes Ereignis als Berechtigungstor: Es wird beobachtet, niemals verworfen, und wenn keine Richtlinienregel übereinstimmt, wird es abgelehnt. Dadurch wird sichergestellt, dass neue Lebenszyklusereignisse von dem Moment an gesteuert werden, in dem sie auftreten, und nicht von dem Moment an, in dem jemand bemerkt, dass dies nicht der Fall ist.

Was passiert, wenn der PEP für eine Hook-Antwort die falsche Drahtform aussendet?

Claude Code ignoriert es stillschweigend. Jedes Hook-Ereignis berücksichtigt ein bestimmtes Ausgabeschema: PreToolUse erwartet hookSpecificOutput.permissionDecision, PermissionRequest erwartet hookSpecificOutput.decision.behavior und andere Gating-Ereignisse erwarten ein Entscheidungs- oder Fortsetzungsfeld der obersten Ebene. Wenn der PEP ein gültiges JSON-Objekt zurückgibt, jedoch im falschen Schema für dieses Ereignis, behandelt Claude Code dies als keine Entscheidung und fährt fort. Aus diesem Grund ist der Wire-Mechanismus Teil des Sicherheitsvertrags und nicht kosmetischer Natur: Eine Ablehnung, die Claude Code ignoriert, ist keine Ablehnung.

Sehen Sie, worauf Ihre Agenten zugreifen können

Olivares AI ist die offene, selbstgehostete Plattform für Ihre KI-Landschaft. Betreiben Sie sie auf Ihrer eigenen Infrastruktur und erhalten Sie die Zugriffskarte, nach der Ihre Security- und Platform-Teams seit Langem fragen.