Aller au contenu

Claude Code

À l'intérieur des hooks PEP : politique de refus de fermeture à l'intérieur de Claude Code

Par Olivares AI 10 min de lecture

Une équipe de plate-forme active Claude Code dans l’ensemble de son organisation d’ingénierie. Pour appliquer la politique sur les appels d’outils, ils connectent un serveur hook - un processus local qui reçoit les événements hook sur stdin et renvoie une décision JSON sur stdout. La première semaine s’annonce bien. PreToolUse se déclenche avant chaque appel d’outil, le hook renvoie deny sur tout ce qui touche à la production, et l’équipe pense que l’application fonctionne.

Ensuite, Claude Code propose une nouvelle version. PermissionRequest commence à tirer aux côtés de PreToolUse. Le serveur hook renvoie le même JSON permissionDecision qu’il renvoyait déjà. Claude Code accepte la réponse, l’analyse, ne trouve aucun champ reconnu pour cet événement et procède comme si aucune décision n’avait été prise. Le refus est silencieusement ignoré. Le point de contrôle de l’équipe est désormais un mur contenant un gap, et rien dans les journaux ne l’indique.

C’est le problème qu’un PEP gouverné doit résoudre : le cycle de vie du hook de Claude Code n’est pas un événement avec un seul format de fil. Ce sont approximately 30 événements, chacun avec un schéma de sortie différent que l’exécution honore, et une erreur dans le schéma est impossible à distinguer du silence.

Le mécanisme filaire est le contrat de sécurité

La raison pour laquelle un seul champ permissionDecision ne fonctionne pas partout est que les événements hook de Claude Code ont évolué indépendamment. La forme de sortie qui déclenche un appel d’outil n’est pas la forme qui déclenche une soumission d’invite, ni la forme qui arrête une tâche.

Il existe six mécanismes filaires distincts :

MécanismeForme de sortieÉvénements
permissionDecisionhookSpecificOutput.permissionDecision (autoriser/deny/ask/defer)Utilisation du pré-outil
permissionBehaviorhookSpecificOutput.decision.behavior (autoriser/deny)Demande d’autorisation
topLevelDecisionNiveau supérieur decision: "block" + reasonUserPromptSubmit, UserPromptExpansion, PreCompact, ConfigChange, PostToolBatch
continueFalsecontinue: false + stopReasonTaskCreated, TaskCompleted, TeammateIdle
postToolUsedecision: "block" (bloque le traitement ultérieur ; outil déjà exécuté)Utilisation de l’outil de publication
neutralPas de blocage exécutoireTous les événements context/observe, plus les événements inversés comme Stop

Si un PEP renvoie permissionDecision: "deny" sur un événement PermissionRequest, Claude Code l’ignore : cet événement attend decision.behavior, pas permissionDecision. Le JSON est valide, la réponse HTTP est 200 et le refus n’existe pas. Il ne s’agit pas d’un cas limite théorique ; il s’agit du contrat de câblage documenté, vérifié par rapport à code.claude.com/docs/en/hooks.

Classification à trois voies : gating, contexte, observer

Tous les événements hook ne peuvent pas déclencher une action. Essayer de nier un SessionEnd ou un Notification n’a aucun sens : ces événements sont informatifs et n’entraînent aucun contrôle de décision. Essayer de refuser un événement Stop est pire que dénué de sens : decision: "block" sur Stop maintient l’agent en marche, ce qui est l’opposé d’un arrêt de sécurité.

Le PEP classe chaque événement de crochet reconnu dans l’une des trois catégories suivantes :

Les événements Gating comportent une décision qui peut autoriser, refuser ou bloquer une action. Ils constituent la surface d’application. Mais tous les événements de gating ne sont pas également applicables : Stop et SubagentStop sont classés comme gating dans la propre taxonomie de Claude Code, mais leur sémantique de bloc est inversée (bloc = continuer à fonctionner), de sorte que le PEP les traite comme neutres : il n’émet jamais de bloc qui maintiendrait un agent en vie contre l’intention de l’opérateur. De même, Elicitation et ElicitationResult ont une classification de déclenchement mais ne disposent pas d’un mécanisme d’action filaire dans la version actuelle, de sorte que le PEP les définit par défaut sur neutre plutôt que de prétendre que l’application existe là où ce n’est pas le cas.

Les événements Context permettent au PEP d’injecter additionalContext ou de réécrire la sortie, mais ne peuvent pas véritablement bloquer l’action. PostToolUse est l’exception partial : il peut bloquer le traitement ultérieur d’une sortie signalée, mais l’outil a déjà été exécuté. Les autres (PermissionDenied, MessageDisplay, SessionStart, Setup, SubagentStart, PostCompact, InstructionsLoaded, PostToolUseFailure) sont des observations contextuelles : utiles pour enrichir la vue du modèle, pas pour l’arrêter.

Les événements Observer (Notification, SessionEnd, StopFailure, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove) n’ont aucun contrôle de décision. Le PEP les enregistre pour le chemin de télémétrie et l’inventaire SIEM, répond de manière neutre et passe à autre chose.

Cette classification n’est pas consultative. Il détermine si le décideur racine de composition applique une règle de politique (gating + exécutoire), injecte un contexte (contexte) ou observe simplement (observer). Se tromper signifie soit une fausse application (renvoyer un refus qui est ignoré), soit une application manquée (traiter un événement de déclenchement comme une observation).

La carte hookSpecs : source unique de vérité

La classification, le mécanisme filaire et le drapeau d’applicabilité résident sur une seule carte. Il s’agit du code réel utilisé par le connecteur : le moteur de rendu de réponse HTTP et le décideur de racine de composition lisent :

type hookSpec struct {
    class       string   // "gating" | "context" | "observe"
    mech        hookMech
    enforceable bool
}

var hookSpecs = map[string]hookSpec{
    // GATING — un retour de hook peut appliquer allow/deny/block à l’action.
    "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}, // inversé
    "SubagentStop":        {"gating", mechNeutral, false}, // inversé
    "Elicitation":         {"gating", mechNeutral, false}, // non câblé dans v1
    "ElicitationResult":   {"gating", mechNeutral, false},
    // CONTEXT — additionalContext / réécriture de sortie, sans véritable blocage.
    "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 — aucun contrôle de décision.
    "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},
}

Trente événements, chacun avec exactement une classification, un mécanisme filaire et un verdict d’applicabilité. Le moteur de rendu consulte hookMechFor(event) pour décider quelle forme JSON émettre. Le décideur consulte HookEnforcementFor(event) pour décider s’il doit appliquer une règle de politique, injecter du contexte ou observer. Tous deux lisent la même carte, ils ne peuvent donc pas être en désaccord.

PEP à crochet fermé refusé : classification des événements et flux du mécanisme de fil

Le défaut de fermeture refusée

La fonction la plus importante du fichier comporte quatre lignes :

func hookSpecFor(event string) hookSpec {
    if s, ok := hookSpecs[event]; ok {
        return s
    }
    return hookSpec{class: "unknown", mech: mechPermissionDecision, enforceable: true}
}

Un événement qui ne figure pas dans la carte (car Claude Code a envoyé un nouvel événement de hook que le connecteur n’a pas encore classé) est traité comme unknown, se voit attribuer le mécanisme filaire mechPermissionDecision et est marqué comme exécutable. Il s’agit de la valeur par défaut de refus fermé : un événement non reconnu est une porte d’autorisation, pas un relais silencieux. Si aucune règle de stratégie ne correspond, la posture par défaut configurée par l’opérateur s’applique, qui dans un déploiement gouverné est le refus.

L’alternative - passer par défaut à neutre ou observer - signifierait que chaque nouvel événement hook introduit par Claude Code est incontrôlé jusqu’à ce que quelqu’un le remarque et l’ajoute à la carte. Dans un modèle fermé par refus, le nouvel événement est gouverné à partir du moment où il se déclenche, même si la classification est conservatrice. Un faux refus sur un nouvel événement est visible et réparable ; une autorisation silencieuse est invisible et peut persister pendant des mois.

La structure HookEnforcement exporte cette classification afin que le décideur puisse distinguer trois niveaux d’événements de déclenchement :

  • Porte classique (PreToolUse, PermissionRequest, PostToolUse et tout événement inconnu) : lorsqu’aucune règle gouvernée ne correspond, la politique par défaut de l’opérateur s’applique (refus-fermé).
  • Porte de cycle de vie (autres événements de déclenchement exécutoires tels que UserPromptSubmit, TaskCreated) : le décideur applique à la place une valeur par défaut sûre par événement : neutre pour les événements UX/lifecycle, refus pour les événements de mutation d’état.
  • Porte non exécutoire (Stop, SubagentStop, Elicitation) : le décideur renvoie le neutre malgré tout. Émettre un refus que Claude Code interprète comme « continuer à fonctionner » serait le contraire de sécurité.

Distribution : du classement à la flotte

Classer correctement les événements représente la moitié. Obtenir le PEP sur chaque instance Claude Code en est une autre. Le connecteur de paramètres gérés restitue la configuration du hook dans la forme attendue par Claude Code et la distribue sous forme de fichier de paramètres gérés par le serveur — une configuration non modifiable que le plan de contrôle transmet à chaque hôte géré.

Le hook PEP est distribué avec un matcher vide (correspond à tous les outils — aucun outil n’échappe au point d’application) et l’associe à allowManagedHooksOnly pour empêcher les hooks locaux d’un développeur de saper le PEP géré. Sans cet indicateur, un hook au niveau de l’utilisateur pourrait masquer celui géré, et l’application semblerait présente tout en étant contournable. La validation au moment de la création détecte ceci : si une stratégie fournit un hook PEP PreToolUse sans allowManagedHooksOnly, la console génère un avis anti-falsification.

Le chemin de télémétrie est séparé et non planifié : l’exportation OpenTelemetry de Claude Code est activée via des variables d’environnement gérées (CLAUDE_CODE_ENABLE_TELEMETRY, les clés d’exportateur OTEL_*) afin que le plan de contrôle puisse observer l’utilisation de l’abonnement sans inférence de proxy ni toucher aux informations d’identification de l’abonnement. La capture de contenu (OTEL_LOG_USER_PROMPTS, OTEL_LOG_TOOL_CONTENT) est désactivée par défaut ; l’activer est un choix délibéré et signalé, car il envoie le contenu des invites et des outils hors de la machine du développeur, créant ainsi une tâche de résidence et de rédaction que le plan de contrôle doit posséder.

Ce que cela signifie pour un déploiement gouverné

Une équipe exécutant Claude Code avec un PEP gouverné obtient quelques propriétés importantes :

  1. Pas d’autorisation silencieuse. Chaque événement hook est classifié et chaque événement non reconnu est refusé par défaut. Une nouvelle version de Claude Code ne peut pas introduire d’événement de cycle de vie non gouverné.
  2. Forme de fil correcte par événement. Le PEP ne renvoie pas d’objet JSON générique et espère que Claude Code l’honorera. Il renvoie le schéma de sortie exact attendu par l’événement spécifique, car un refus dans le mauvais schéma n’est pas un refus.
  3. Non-application honnête. Les événements qui ne peuvent pas être appliqués (événements d’observation, événements de déclenchement inversé) ne sont pas appliqués de manière simulée. Le PEP les enregistre, revient au neutre et ne donne pas à l’opérateur un faux sentiment de contrôle.
  4. Anti-altération lors de la distribution. La couche de paramètres gérés garantit que le hook PEP n’est pas remplaçable et signale les configurations où il pourrait être compromis.

Le PEP est la moitié d’un modèle d’opération gouverné plus vaste. La carte d’accès, le grand livre d’audit et la couche de stratégie en tant que code qui l’entoure sont traités dans la présentation du produit et la documentation des hooks. Si vous souhaitez voir comment le chemin de télémétrie et la porte d’autorisation se composent, la présentation de l’architecture parcourt les deux.

Articles liés

Questions fréquentes

Pourquoi le PEP refuse-t-il par défaut les événements de hook inconnus au lieu d'autoriser ?

Une autorisation silencieuse sur un événement non reconnu signifie que tout nouveau hook Claude Code livré dans une version future contournerait l'application jusqu'à ce que quelqu'un l'ajoute manuellement à la carte de classification. C’est l’opposé de la posture de sécurité dont un déploiement gouverné a besoin. La valeur par défaut de refus fermé traite un événement inconnu comme une porte d'autorisation : il est observé, jamais abandonné, et si aucune règle de politique ne correspond, il est refusé. Cela garantit que les nouveaux événements du cycle de vie sont régis dès le moment où ils apparaissent, et non à partir du moment où quelqu'un remarque qu'ils ne l'étaient pas.

Que se passe-t-il si le PEP émet une forme de fil incorrecte pour une réponse en crochet ?

Claude Code l'ignore en silence. Chaque événement hook honore un schéma de sortie spécifique : PreToolUse attend hookSpecificOutput.permissionDecision, PermissionRequest attend hookSpecificOutput.decision.behavior et d'autres événements de déclenchement attendent une décision de niveau supérieur ou un champ de continuation. Si le PEP renvoie un objet JSON valide mais dans le mauvais schéma pour cet événement, Claude Code le traite comme une absence de décision et continue. C'est pourquoi le mécanisme filaire fait partie du contrat de sécurité, et non cosmétique : un refus ignoré par Claude Code n'est pas un refus.

Découvrez ce que vos agents peuvent atteindre

Olivares AI est la plateforme ouverte et auto-hébergée pour votre parc informatique d'IA. Déployez-la sur votre propre infrastructure et obtenez la cartographie des accès que réclament vos équipes de sécurité et de plateforme.