Vai al contenuto

Claude Code

All'interno degli hook PEP: policy di negazione chiusa all'interno di Claude Code

Scritto da Olivares AI 9 min di lettura

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:

MeccanismoForma di uscitaEventi
permissionDecisionhookSpecificOutput.permissionDecision (consenti/deny/ask/defer)PreToolUse
permissionBehaviorhookSpecificOutput.decision.behavior (consenti/deny)Richiesta di autorizzazione
topLevelDecisiondecision: "block" + reason di altissimo livelloUserPromptSubmit, UserPromptExpansion, PreCompact, ConfigChange, PostToolBatch
continueFalsecontinue: false + stopReasonAttività creata, attività completata, compagno di squadra inattivo
postToolUsedecision: "block" (blocca ulteriori elaborazioni; strumento già eseguito)PostToolUse
neutralNessun blocco esecutivoTutti 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.

PEP con hook chiuso e negato: classificazione degli eventi e flusso del meccanismo del cablaggio

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

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

Articoli correlati

Domande frequenti

Perché gli eventi hook sconosciuti PEP per impostazione predefinita negano anziché consentire?

Un'autorizzazione silenziosa su un evento non riconosciuto significa che qualsiasi nuovo hook Claude Code fornito in una versione futura ignorerebbe l'applicazione finché qualcuno non lo aggiungerà manualmente alla mappa di classificazione. Questo è l’opposto del livello di sicurezza di cui ha bisogno una distribuzione governata. L'impostazione predefinita Deny-Closed tratta un evento sconosciuto come un cancello di autorizzazione: viene osservato, non viene mai eliminato e, se nessuna regola di policy corrisponde, viene negato. Ciò garantisce che i nuovi eventi del ciclo di vita siano governati dal momento in cui compaiono, non dal momento in cui qualcuno si accorge che non lo sono.

Cosa succede se il PEP emette la forma sbagliata del filo per una risposta al gancio?

Claude Code lo ignora silenziosamente. Ogni evento hook rispetta uno schema di output specifico: PreToolUse prevede hookSpecificOutput.permissionDecision, PermissionRequest prevede hookSpecificOutput.decision.behavior e altri eventi di gating prevedono una decisione di livello superiore o un campo continua. Se il PEP restituisce un oggetto JSON valido ma nello schema sbagliato per quell'evento, Claude Code lo considera come nessuna decisione e procede. Questo è il motivo per cui il meccanismo del filo è parte del contratto di sicurezza, non cosmetico: un rifiuto che Claude Code ignora non è un rifiuto.

Scopri cosa possono raggiungere i tuoi agenti

Olivares AI è la piattaforma aperta e in self-hosting per il tuo parco AI. Distribuiscila sulla tua infrastruttura e ottieni la mappa degli accessi che i tuoi team security e platform richiedono.