Een platformteam maakt Claude Code mogelijk in de hele technische organisatie. Om beleid ten aanzien van toolaanroepen af te dwingen, sluiten ze een hook-server aan: een lokaal proces dat hook-gebeurtenissen ontvangt op stdin en een JSON-beslissing retourneert op stdout. De eerste week ziet er prima uit. PreToolUse vuurt vóór elke gereedschapsoproep, de hook geeft deny terug bij alles wat met productie te maken heeft, en het team is ervan overtuigd dat de handhaving werkt.
Dan levert Claude Code een nieuwe release. PermissionRequest begint te schieten naast PreToolUse. De hook-server retourneert dezelfde permissionDecision JSON die hij al retourneerde. Claude Code accepteert het antwoord, ontleedt het, vindt geen veld dat het herkent voor die gebeurtenis en gaat verder alsof er geen beslissing is genomen. De ontkenning wordt stilzwijgend genegeerd. Het handhavingspunt van het team is nu een muur met een gap erin, en niets in de logboeken zegt dat.
Dit is het probleem dat een bestuurde PEP moet oplossen: de hook-levenscyclus van Claude Code is niet één gebeurtenis met één draadformaat. Het zijn approximately 30 gebeurtenissen, elk met een ander uitvoerschema die de runtime eer aandoet, en een fout in het schema is niet te onderscheiden van stilte.
Het draadmechanisme is het beveiligingscontract
De reden dat een enkel permissionDecision-veld niet overal werkt, is dat de hook-events van Claude Code onafhankelijk zijn geëvolueerd. De uitvoervorm die een tooloproep doorlaat, is niet de vorm die een promptinzending doorlaat, wat niet de vorm is die een taak stopt.
Er zijn zes verschillende draadmechanismen:
| Mechanisme | Uitvoervorm | Evenementen |
|---|---|---|
permissionDecision | hookSpecificOutput.permissionDecision (/deny/ask/defer toestaan) | PreToolGebruik |
permissionBehavior | hookSpecificOutput.decision.behavior (/deny toestaan) | ToestemmingVerzoek |
topLevelDecision | decision: "block" + reason van het hoogste niveau | UserPromptSubmit, UserPromptExpansion, PreCompact, ConfigChange, PostToolBatch |
continueFalse | continue: false + stopReason | TaskCreated, TaskCompleted, TeammateIdle |
postToolUse | decision: "block" (blokkeert verdere verwerking; tool is al uitgevoerd) | PostToolGebruik |
neutral | Geen afdwingbare blokkering | Alle context/observe-gebeurtenissen, plus omgekeerde gebeurtenissen zoals Stop |
Als een PEP permissionDecision: "deny" retourneert op een PermissionRequest-gebeurtenis, negeert Claude Code deze gebeurtenis; die gebeurtenis verwacht decision.behavior, niet permissionDecision. De JSON is geldig, het HTTP-antwoord is 200 en de weigering bestaat niet. Dit is geen theoretisch randgeval; het is het gedocumenteerde draadcontract, geverifieerd aan de hand van code.claude.com/docs/en/hooks.
Driewegclassificatie: poort, context, observeren
Niet elke hook-gebeurtenis kan een actie opleveren. Proberen een SessionEnd of een Notification te ontkennen is zinloos; deze gebeurtenissen zijn informatief en hebben geen beslissingscontrole. Proberen een Stop-gebeurtenis te ontkennen is erger dan zinloos: decision: "block" op Stop zorgt ervoor dat de agent blijft draaien, wat het tegenovergestelde is van een veiligheidsstop.
De PEP classificeert elke erkende hook-gebeurtenis in een van de drie categorieën:
Gating-gebeurtenissen bevatten een beslissing die een actie kan toestaan, weigeren of blokkeren. Zij vormen het handhavingsoppervlak. Maar niet alle poortgebeurtenissen zijn even afdwingbaar: Stop en SubagentStop worden geclassificeerd als poort in de eigen taxonomie van Claude Code, maar hun bloksemantiek is omgekeerd (blok = blijven draaien), dus de PEP behandelt ze als neutraal - het zendt nooit een blok uit dat een agent in leven zou houden, tegen de bedoeling van de operator in. Op dezelfde manier hebben Elicitation en ElicitationResult een poortclassificatie, maar missen ze een vast actiemechanisme in de huidige versie, dus de PEP stelt ze standaard in op neutraal in plaats van te doen alsof er handhaving bestaat waar dat niet het geval is.
Dankzij Context-gebeurtenissen kan de PEP additionalContext injecteren of uitvoer herschrijven, maar kan de actie niet echt blokkeren. PostToolUse is de partial-uitzondering: deze kan verdere verwerking van een gemarkeerde uitvoer blokkeren, maar de tool is al uitgevoerd. De rest (PermissionDenied, MessageDisplay, SessionStart, Setup, SubagentStart, PostCompact, InstructionsLoaded, PostToolUseFailure) zijn observatie-met-context: handig voor het verrijken van de modellen van het model. bekijken, niet om het tegen te houden.
Observeer gebeurtenissen (Notification, SessionEnd, StopFailure, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove) hebben helemaal geen beslissingscontrole. De PEP registreert ze voor het telemetriepad en de SIEM-inventaris, antwoordt neutraal en gaat verder.
Deze classificatie is niet adviserend. Het bepaalt of de compositie-root-beslisser een beleidsregel toepast (gating + afdwingbaar), context injecteert (context) of alleen maar observeert (observeren). Als u het fout doet, betekent dit ofwel valse handhaving (het retourneren van een weigering die wordt genegeerd) of gemiste handhaving (het behandelen van een poortgebeurtenis als observeren).
De hookSpecs-kaart: enige bron van waarheid
De classificatie, het draadmechanisme en de afdwingbaarheidsvlag bevinden zich op één kaart. Dit is de daadwerkelijke code die de connector gebruikt: zowel de HTTP-antwoordrenderer als de compositie-rootbeslisser lezen ervan:
type hookSpec struct {
class string // "gating" | "context" | "observe"
mech hookMech
enforceable bool
}
var hookSpecs = map[string]hookSpec{
// GATING — een hook-return kan allow/deny/block op de actie toepassen.
"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}, // omgekeerd
"SubagentStop": {"gating", mechNeutral, false}, // omgekeerd
"Elicitation": {"gating", mechNeutral, false}, // niet aangesloten in v1
"ElicitationResult": {"gating", mechNeutral, false},
// CONTEXT — additionalContext / uitvoer herschrijven, geen echte blokkering.
"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 — geen beslissingscontrole.
"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},
}
Dertig evenementen, elk met precies één classificatie, één overschrijvingsmechanisme en één uitspraak over de uitvoerbaarheid. De renderer raadpleegt hookMechFor(event) om te beslissen welke JSON-vorm moet worden uitgezonden. De beslisser raadpleegt HookEnforcementFor(event) om te beslissen of een beleidsregel moet worden toegepast, context moet worden toegevoegd of moet worden geobserveerd. Beiden lezen van dezelfde kaart, dus ze kunnen het niet oneens zijn.
De deny-closed-standaard
De belangrijkste functie in het bestand is vier regels lang:
func hookSpecFor(event string) hookSpec {
if s, ok := hookSpecs[event]; ok {
return s
}
return hookSpec{class: "unknown", mech: mechPermissionDecision, enforceable: true}
}
Een gebeurtenis die niet op de kaart staat (omdat Claude Code een nieuwe hook-gebeurtenis heeft verzonden die de connector nog niet heeft geclassificeerd) wordt behandeld als unknown, krijgt het draadmechanisme mechPermissionDecision toegewezen en wordt gemarkeerd als afdwingbaar. Dit is de standaardwaarde voor weigeren en sluiten: een niet-herkende gebeurtenis is een toestemmingspoort en geen stille doorgang. Als er geen beleidsregel overeenkomt, is de geconfigureerde standaardhouding van de operator van toepassing, die in een beheerde implementatie deny is.
Het alternatief – standaard neutraal of observerend – zou betekenen dat elke nieuwe hook-gebeurtenis die Claude Code introduceert ongecontroleerd is totdat iemand het opmerkt en aan de kaart toevoegt. In een deny-closed-model wordt de nieuwe gebeurtenis bepaald vanaf het moment dat deze plaatsvindt, zelfs als de classificatie conservatief is. Een valse ontkenning van een nieuwe gebeurtenis is zichtbaar en herstelbaar; een stille toestemming is onzichtbaar en kan maanden aanhouden.
De HookEnforcement-structuur exporteert deze classificatie zodat de beslisser drie niveaus van poortgebeurtenissen kan onderscheiden:
- Klassieke poort (
PreToolUse,PermissionRequest,PostToolUseen elke onbekende gebeurtenis): wanneer geen enkele regel overeenkomt, is het standaardbeleid van de operator van toepassing (weigeren-gesloten). - Levenscycluspoort (andere afdwingbare poortgebeurtenissen zoals
UserPromptSubmit,TaskCreated): de beslisser past in plaats daarvan een veilige standaard per gebeurtenis toe: neutraal voor UX/lifecycle-gebeurtenissen, weigeren voor statusmutatiegebeurtenissen. - Niet-afdwingbare poort (
Stop,SubagentStop,Elicitation): de beslisser keert hoe dan ook terug naar neutraal. Het uiten van een ontkenning die Claude Code interpreteert als “door blijven rennen” zou het tegenovergestelde zijn van veilig.
Distributie: van classificatie tot vloot
Het correct classificeren van gebeurtenissen is de helft. De andere is om de PEP op elke Claude Code-instantie te krijgen. De connector voor beheerde instellingen geeft de hook-configuratie weer in de vorm die Claude Code verwacht en distribueert deze als een door de server beheerd instellingenbestand: een niet-overschrijfbare configuratie die het besturingsvlak naar elke beheerde host pusht.
De PEP-hook wordt gedistribueerd met een lege matcher (komt overeen met alle tools - geen enkele tool ontsnapt aan het handhavingspunt) en koppelt deze aan allowManagedHooksOnly om te voorkomen dat de lokale hooks van een ontwikkelaar de beheerde PEP ondermijnen. Zonder die vlag zou een hook op gebruikersniveau de beheerde hook kunnen overschaduwen, en handhaving zou aanwezig lijken terwijl het omzeilbaar zou zijn. De validatie tijdens het schrijven merkt dit op: als een beleid een PreToolUse PEP-hook verzendt zonder allowManagedHooksOnly, geeft de console een anti-sabotageadvies.
Het telemetriepad is gescheiden en gepland: de OpenTelemetry-export van Claude Code wordt mogelijk gemaakt via beheerde omgevingsvariabelen (CLAUDE_CODE_ENABLE_TELEMETRY, de OTEL_*-exportsleutels), zodat het besturingsvlak het abonnementsgebruik kan observeren zonder proxy-inferentie of de abonnementsgegevens aan te raken. Het vastleggen van inhoud (OTEL_LOG_USER_PROMPTS, OTEL_LOG_TOOL_CONTENT) is standaard uitgeschakeld; het inschakelen ervan is een bewuste, gemarkeerde keuze omdat het prompt- en toolinhoud van de machine van de ontwikkelaar verzendt, waardoor een residentie- en redactieplicht ontstaat die het controlevlak moet bezitten.
Wat dit betekent voor een beheerste implementatie
Een team dat Claude Code draait met een bestuurde PEP krijgt een paar eigenschappen die er toe doen:
- Geen stil toestaan. Elke hook-gebeurtenis is geclassificeerd en elke niet-herkende gebeurtenis wordt standaard geweigerd. Een nieuwe Claude Code-release kan geen niet-beheerde levenscyclusgebeurtenis introduceren.
- Juiste draadvorm per gebeurtenis. De PEP retourneert geen generiek JSON-object en hoopt dat Claude Code dit respecteert. Het retourneert het exacte uitvoerschema dat de specifieke gebeurtenis verwacht, omdat een weigering in het verkeerde schema geen weigering is.
- Eerlijk niet-handhaving. Gebeurtenissen die niet kunnen worden afgedwongen (observeer gebeurtenissen, omgekeerde poortgebeurtenissen) worden niet alsof ze worden afgedwongen. De PEP registreert ze, keert neutraal terug en geeft de operator geen vals gevoel van controle.
- Anti-sabotage bij distributie. De laag met beheerde instellingen zorgt ervoor dat de PEP-hook niet kan worden overschreven en markeert configuraties waar deze kan worden ondermijnd.
De PEP is de handhavingshelft van een groter beheersmodel. De toegangskaart, het auditgrootboek en de laag beleid-als-code eromheen worden behandeld in het productoverzicht en de hooksdocumentatie. Als je wilt zien hoe het telemetriepad en de toestemmingspoort zijn samengesteld, loopt het architectuuroverzicht door beide.