Un team della piattaforma abilita Claude Code in tutta la sua organizzazione di ingegneria. Per applicare la policy sulle chiamate agli strumenti, collegano un server di hook, un processo locale che riceve eventi di hook su stdin e restituisce una decisione JSON su stdout. La prima settimana sembra andare bene. PreToolUse si attiva prima di ogni chiamata dello strumento, l’hook restituisce deny su qualsiasi cosa tocchi la produzione e il team ritiene che l’applicazione funzioni.
Quindi Claude Code fornisce una nuova versione. PermissionRequest inizia a sparare insieme a PreToolUse. Il server hook restituisce lo stesso permissionDecision JSON che stava già restituendo. Claude Code accetta la risposta, la analizza, non trova alcun campo riconosciuto per quell’evento e procede come se non fosse stata data alcuna decisione. Il rifiuto viene silenziosamente ignorato. Il punto di controllo della squadra ora è un muro con uno gap e nulla nei registri lo dice.
Questo è il problema che un PEP regolamentato deve risolvere: il ciclo di vita dell’hook di Claude Code non è un evento con un formato di cavo unico. Si tratta di approximately 30 eventi, ciascuno con uno schema di output diverso onorato dal runtime e un errore nello schema è indistinguibile dal silenzio.
Il meccanismo del filo è il contratto di sicurezza
Il motivo per cui un singolo campo permissionDecision non funziona ovunque è che gli eventi hook di Claude Code si sono evoluti in modo indipendente. La forma di output che abilita la chiamata di uno strumento non è la forma che abilita un invio immediato, che non è la forma che interrompe un’attività.
Esistono sei distinti meccanismi di filo:
| Meccanismo | Forma di uscita | Eventi |
|---|---|---|
permissionDecision | hookSpecificOutput.permissionDecision (consenti/deny/ask/defer) | PreToolUse |
permissionBehavior | hookSpecificOutput.decision.behavior (consenti/deny) | Richiesta di autorizzazione |
topLevelDecision | decision: "block" + reason di altissimo livello | UserPromptSubmit, UserPromptExpansion, PreCompact, ConfigChange, PostToolBatch |
continueFalse | continue: false + stopReason | Attività creata, attività completata, compagno di squadra inattivo |
postToolUse | decision: "block" (blocca ulteriori elaborazioni; strumento già eseguito) | PostToolUse |
neutral | Nessun blocco esecutivo | Tutti gli eventi di contesto/observe, più eventi invertiti come Stop |
Se un PEP restituisce permissionDecision: "deny" su un evento PermissionRequest, Claude Code lo ignora: quell’evento prevede decision.behavior, non permissionDecision. Il JSON è valido, la risposta HTTP è 200 e il rifiuto non esiste. Questo non è un caso limite teorico; è il contratto di bonifico documentato, verificato rispetto a code.claude.com/docs/en/hooks.
Classificazione a tre vie: gating, contesto, osservazione
Non tutti gli eventi hook possono bloccare un’azione. Cercare di negare uno SessionEnd o uno Notification non ha senso: questi eventi sono informativi e non comportano alcun controllo decisionale. Cercare di negare un evento Stop è peggio che privo di significato: decision: "block" su Stop mantiene l’agente in esecuzione, il che è l’opposto di un arresto di sicurezza.
Il PEP classifica ogni evento hook riconosciuto in una delle tre categorie:
Gli eventi Gating portano con sé una decisione che può consentire, negare o bloccare un’azione. Sono la superficie di applicazione. Ma non tutti gli eventi di gating sono ugualmente applicabili: Stop e SubagentStop sono classificati come gating nella tassonomia di Claude Code, ma la loro semantica di blocco è invertita (blocco = continua a funzionare), quindi il PEP li tratta come neutrali: non emette mai un blocco che manterrebbe in vita un agente contro l’intento dell’operatore. Allo stesso modo, Elicitation e ElicitationResult hanno una classificazione gating ma non dispongono di un meccanismo di azione cablato nella versione attuale, quindi il PEP li imposta per impostazione predefinita su neutrali anziché fingere che esista un’applicazione dove non è così.
Gli eventi Contesto consentono al PEP di iniettare additionalContext o di riscrivere l’output, ma non possono realmente bloccare l’azione. PostToolUse è l’eccezione partial: può bloccare l’ulteriore elaborazione di un output contrassegnato, ma lo strumento è già stato eseguito. Il resto (PermissionDenied, MessageDisplay, SessionStart, Setup, SubagentStart, PostCompact, InstructionsLoaded, PostToolUseFailure) sono osservabili nel contesto: utili per arricchire la vista del modello, non per fermarlo.
Gli eventi Osserva (Notification, SessionEnd, StopFailure, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove) non hanno alcun controllo decisionale. Il PEP li registra per il percorso telemetrico e l’inventario SIEM, risponde in modo neutrale e va avanti.
Questa classificazione non è consultiva. Determina se il decisore della radice della composizione applica una regola politica (gating + applicabile), inserisce il contesto (contesto) o semplicemente osserva (osserva). Sbagliare significa o falsa applicazione (restituire un rifiuto che viene ignorato) o mancata applicazione (trattare un evento di controllo come osservazione).
La mappa hookSpecs: unica fonte di verità
La classificazione, il meccanismo di collegamento e il flag di applicabilità risiedono in un’unica mappa. Questo è il codice effettivo utilizzato dal connettore: sia il renderer della risposta HTTP che il decisore root della composizione leggono da esso:
type hookSpec struct {
class string // "gating" | "context" | "observe"
mech hookMech
enforceable bool
}
var hookSpecs = map[string]hookSpec{
// GATING — il ritorno di un hook può applicare allow/deny/block all’azione.
"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}, // invertito
"SubagentStop": {"gating", mechNeutral, false}, // invertito
"Elicitation": {"gating", mechNeutral, false}, // non cablato in v1
"ElicitationResult": {"gating", mechNeutral, false},
// CONTEXT — additionalContext / riscrittura dell’output, nessun blocco reale.
"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 — nessun controllo delle decisioni.
"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},
}
Trenta eventi, ciascuno con esattamente una classificazione, un meccanismo di trasmissione e un verdetto di esecutività. Il renderer consulta hookMechFor(event) per decidere quale forma JSON emettere. Il decisore consulta HookEnforcementFor(event) per decidere se applicare una regola di policy, inserire contesto o osservare. Entrambi leggono dalla stessa mappa, quindi non possono essere in disaccordo.
L’impostazione predefinita con negazione chiusa
La funzione più importante nel file è lunga quattro righe:
func hookSpecFor(event string) hookSpec {
if s, ok := hookSpecs[event]; ok {
return s
}
return hookSpec{class: "unknown", mech: mechPermissionDecision, enforceable: true}
}
Un evento che non è presente nella mappa, poiché Claude Code ha inviato un nuovo evento di hook che il connettore non ha ancora classificato, viene trattato come unknown, gli viene assegnato il meccanismo di cablaggio mechPermissionDecision e contrassegnato come applicabile. Questa è l’impostazione predefinita di negazione chiusa: un evento non riconosciuto è un gate di autorizzazione, non un pass-through silenzioso. Se nessuna regola dei criteri corrisponde, si applica la postura predefinita configurata dell’operatore, che in una distribuzione governata è Nega.
L’alternativa - passare a neutrale o osservare - significherebbe che ogni nuovo evento hook introdotto da Claude Code non è controllato finché qualcuno non se ne accorge e lo aggiunge alla mappa. In un modello negato, il nuovo evento è governato dal momento in cui si attiva, anche se la classificazione è conservativa. Una falsa negazione su un nuovo evento è visibile e risolvibile; un consenso silenzioso è invisibile e può persistere per mesi.
La struttura HookEnforcement esporta questa classificazione in modo che il decisore possa distinguere tre livelli di eventi di gating:
- Cancello classico (
PreToolUse,PermissionRequest,PostToolUsee qualsiasi evento sconosciuto): quando nessuna regola governata corrisponde, si applica la policy predefinita dell’operatore (deny-closed). - Porta del ciclo di vita (altri eventi di gating applicabili come
UserPromptSubmit,TaskCreated): il decisore applica invece un valore predefinito sicuro per evento: neutrale per gli eventi UX/lifecycle, negato per gli eventi di mutazione di stato. - Cancello non applicabile (
Stop,SubagentStop,Elicitation): il decisore ritorna comunque neutrale. Emettere un rifiuto che Claude Code interpreta come “continua a correre” sarebbe l’opposto di sicuro.
Distribuzione: dalla classificazione alla flotta
Classificare correttamente gli eventi è la metà. L’altro è ottenere il PEP su ogni istanza Claude Code. Il connettore delle impostazioni gestite esegue il rendering della configurazione dell’hook nella forma prevista da Claude Code e la distribuisce come file di impostazioni gestite dal server: una configurazione non sovrascrivibile che il piano di controllo invia a ogni host gestito.
L’hook PEP viene distribuito con un matcher vuoto (corrisponde a tutti gli strumenti: nessuno strumento sfugge al punto di applicazione) e lo accoppia con allowManagedHooksOnly per impedire agli hook locali di uno sviluppatore di indebolire il PEP gestito. Senza quel flag, un hook a livello utente potrebbe oscurare quello gestito e l’applicazione sembrerebbe presente pur essendo aggirabile. La convalida in fase di creazione rileva questo: se una policy fornisce un hook PEP PreToolUse senza allowManagedHooksOnly, la console genera un avviso anti-manomissione.
Il percorso di telemetria è separato e non pianificato: l’esportazione OpenTelemetry di Claude Code è abilitata tramite variabili di ambiente gestito (CLAUDE_CODE_ENABLE_TELEMETRY, le chiavi di esportazione OTEL_*) in modo che il piano di controllo possa osservare l’utilizzo dell’abbonamento senza inoltrare inferenze o toccare le credenziali di abbonamento. L’acquisizione del contenuto (OTEL_LOG_USER_PROMPTS, OTEL_LOG_TOOL_CONTENT) è disattivata per impostazione predefinita; attivarlo è una scelta deliberata e segnalata perché invia il contenuto dei prompt e degli strumenti dalla macchina dello sviluppatore, creando un compito di residenza e redazione che il piano di controllo deve possedere.
Cosa significa per una distribuzione governata
Un team che esegue Claude Code con un PEP regolamentato ottiene alcune proprietà che contano:
- Nessun consenso silenzioso. Ogni evento hook è classificato e ogni evento non riconosciuto viene negato per impostazione predefinita. Una nuova versione Claude Code non può introdurre un evento del ciclo di vita non governato.
- Forma del cavo corretta per evento. Il PEP non restituisce un oggetto JSON generico e spera che Claude Code lo onori. Restituisce l’esatto schema di output previsto dall’evento specifico, perché un rifiuto nello schema sbagliato non è un rifiuto.
- Onesta non applicazione. Gli eventi che non possono essere applicati (osservare eventi, eventi con gating invertito) non vengono applicati in modo simulato. Il PEP li registra, ritorna neutrale e non dà all’operatore un falso senso di controllo.
- Anti-manomissione alla distribuzione. Il livello delle impostazioni gestite garantisce che il gancio PEP non sia sovrascrivibile e segnala le configurazioni in cui potrebbe essere compromesso.
Il PEP è la metà esecutiva di un modello operativo governato più ampio. La mappa di accesso, il registro di controllo e il livello policy-as-code attorno ad esso sono trattati nella panoramica del prodotto e nella documentazione degli hook. Se vuoi vedere come si compongono il percorso di telemetria e il gate di autorizzazione, la panoramica dell’architettura li esamina entrambi.