Hooks — référence

Événements hooks

Hook Outil Garde Ce qu’il impose En cas d’échec interne
SessionStart — — Exporte SKRAFT_PLUGIN_ROOT vers les appels Bash suivants (Claude Code, via CLAUDE_ENV_FILE) ; indique le chemin du plugin et le pipeline actif dans le contexte de session ; purge le journal d’audit et les signaux d’état obsolètes Autorise
SubagentStart — G2 Indique à l’agent qui démarre ses skills obligatoires (verify ou eager) ; intègre le contenu des skills eager ; exclut les skills on-demand Autorise
PreToolUse Agent, Task G1 Un agent de phase n’est dispatché que si la phase enregistrée le permet : le spécialiste dans la phase ouverte, son reviewer une fois un artefact enregistré Bloque, pour un agent de phase
PreToolUse Agent, Task Provenance Aucun agent ne se dispatche lui-même ; un agent au dispatcher déclaré n’est dispatché que par lui Autorise
PreToolUse Agent, Task G9 Le prompt de dispatch d’un agent de phase du pipeline nomme au moins un chemin enregistré pour chaque entrée suivie obligatoire déjà présente dans l’état, et la revue précédente en rework ou re-review Autorise
PreToolUse Bash, Write, Edit, MultiEdit, NotebookEdit G7 Aucune écriture directe dans le state.json d’un pipeline, son journal d’exécution ou le pointeur .active-slug, quelle que soit la phase Refuse si le payload nomme un state.json suivi
PreToolUse idem G8 L’orchestrateur n’écrit jamais src/ ni tests/, quelle que soit la phase ; un appelant que le payload ne nomme pas passe Autorise
PostToolUse Agent, Task G6 Au retour d’un agent de phase, l’orchestrateur reçoit quoi enregistrer et quoi dispatcher ensuite Autorise
PostToolUse Read G3 Chaque lecture d’un SKILL.md est inscrite au journal d’audit Autorise
SubagentStop — G3 Un sous-agent dont le transcript ne montre aucun chargement d’un skill obligatoire (appel de l’outil skill, ou lecture de son SKILL.md) est renvoyé au travail ; les skills on-demand ne sont pas obligatoires ; un sous-agent déjà renvoyé est laissé partir Autorise

Les deux manifestes du plugin portent les mêmes entrées, et chaque entrée exécute src/cli/hook.mjs (src/cli/housekeeping.mjs pour SessionStart). Copilot CLI envoie ses propres noms d’outils (bash, create, str_replace, view, …) ; adapters/api/hooks/harness-input.mjs les traduit vers les noms ci-dessus avant toute garde. Un lot Copilot toolCalls est gardé appel par appel ; un seul appel refusé refuse le lot.

Chaque événement d’outil a une seule entrée, sans matcher : VS Code ignore les matchers et exécuterait toutes les entrées d’un événement à chaque appel d’outil. src/cli/hook.mjs lit le nom de l’outil dans le payload et sort avant de charger la moindre garde quand l’appel ne concerne aucun des outils ci-dessus.

G7 et G8 lisent une commande shell à sa forme : redirections, tee, verbes qui réécrivent ou copient, sed et perl en place, scripts en ligne node -e ou python -c, derrière des affectations VAR=valeur et des enveloppes comme sudo ou env. Une écriture cachée derrière bash -c, une variable, un sous-shell ou find -delete n’est pas reconnue.

Politiques de skills

Politique Injection G2 au démarrage Conformité G3 à l’arrêt Trace G3 de lecture
verify Listée comme obligatoire Exigée Oui
eager Listée comme obligatoire et intégrée Exigée Oui
on-demand Non injectée Non exigée Oui

Porte de phase (CLI d’état, G4/G5)

La complétude d’une phase n’est pas un hook. L’orchestrateur enregistre artefacts et verdicts après le retour d’un sous-agent ; le contrôle a donc lieu à la clôture de la phase : state.mjs transition et state.mjs close-phase refusent avec PHASE_GATE sauf si

La porte échoue fermée : une phase qui ne peut pas être contrôlée ne se clôt pas.

Handoff et durée via le CLI d’état

Sous-commande Sortie
handoff --agent <name> Bloc Markdown de handoff pour le prochain dispatch d’agent de phase : entrées obligatoires résolues en chemins enregistrés, entrées de contexte, artefacts en revue, revue précédente et sorties précédentes en retry
timeline Durées par phase depuis le journal de dispatch : temps spécialiste, temps reviewer, autres sous-agents, tentatives, blocages de conformité skill et runs de mutation

Statut de vérification

Chaque garde ci-dessus est couverte par des tests unitaires et d’acceptation sous tests/skraft-framework/. Une exécution sur un harness réel est une preuve distincte : scripts/copilot-hook-smoke.mjs et scripts/claude-plugin-smoke.mjs pilotent une vraie session avec une commande shell autorisée et un refus G7. Le dernier passage enregistré est Copilot CLI 1.0.83 pour ces deux sondes ; les autres gardes n’ont aucune preuve en session réelle, et les évaluations Vally ne chargent pas les hooks du plugin.

Types de décision (vocabulaire interne)

Les handlers retournent l’une des quatre décisions construites par plugins/skraft-framework/src/adapters/api/hooks/decision.mjs :

Décision Effet Quand l’utiliser
allow L’outil s’exécute normalement Payload conforme, aucun invariant violé
deny Refus non-bloquant — l’agent peut reformuler Violation détectée, récupérable
block Blocage immédiat — pipeline interrompu Violation critique, irrécupérable
additionalContext L’outil s’exécute mais l’agent reçoit un contexte supplémentaire Avertissement ou info d’audit
allow()                                  // { decision: 'allow' }
deny('Raison du refus')                  // { decision: 'deny', message: … }
block('Raison du blocage')               // { decision: 'block', message: … }
additionalContext('Information ajoutée') // { decision: 'additionalContext', context: … }

Ce vocabulaire n’atteint jamais le harness. C’est le langage propre au framework, traduit à la frontière du CLI par plugins/skraft-framework/src/adapters/api/hooks/harness-output.mjs.

Format de fil harness (ce qui est réellement écrit sur stdout)

Les deux harnesses typent la clé racine decision comme "approve" | "block". Écrire {"decision":"allow"} ou {"decision":"deny"} invalide le payload entier — Claude Code journalise Hook JSON output validation failed — (root): Invalid input, jette la sortie et laisse l’outil s’exécuter. Une garde qui émet le vocabulaire interne est donc inerte.

Une seule enveloppe satisfait les deux runtimes : Claude Code lit hookSpecificOutput et retire les clés racine inconnues, Copilot CLI lit les clés racine et ignore hookSpecificOutput.

Décision Événement stdout
allow tous (rien — un stdout vide n’est jamais parsé, il ne peut donc jamais échouer à la validation)
deny / block PreToolUse permissionDecision + permissionDecisionReason, à la racine et dans hookSpecificOutput
deny / block tout autre { "decision": "block", "reason": … }
additionalContext tous additionalContext à la racine et dans hookSpecificOutput
// deny / block sur PreToolUse — seul l'outil est refusé, la session continue
{
  "permissionDecision": "deny",
  "permissionDecisionReason": "Raison du refus",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Raison du refus"
  }
}

// deny / block sur tout autre événement
{ "decision": "block", "reason": "Raison du blocage" }

// additionalContext
{
  "additionalContext": "Information ajoutée",
  "hookSpecificOutput": { "hookEventName": "PostToolUse", "additionalContext": "Information ajoutée" }
}

hookSpecificOutput.hookEventName doit correspondre à l’événement en cours, sinon Claude Code rejette le bloc. Un block sur PreToolUse est mappé sur permissionDecision: "deny" et jamais sur continue: false : un bug de hook ne doit pas figer le pipeline.

Si le hook n’écrit rien ou exit 0 sans output, les deux runtimes interprètent comme allow.

Normalisation du payload

Tous les payloads entrants sont normalisés en camelCase avant routage :

Format entrant Résultat
tool_name (snake_case) toolName
ToolName (PascalCase) toolName
toolName (camelCase) toolName (inchangé)
File_Path (mixte) filePath

Implémenté dans plugins/skraft-framework/src/adapters/api/hooks/payload.mjs.

Variables d’environnement

Les hooks et les CLI lisent ces variables ; aucune n’est obligatoire.

Variable Effet Défaut
SKRAFT_PLUGIN_ROOT Emplacement du plugin pour les commandes shell des agents ; exportée par SessionStart Posée par le hook sur Claude Code ; indiquée dans le contexte de session sur les deux harnesses
SKRAFT_PROJECT_SLUG Pipeline sur lequel agissent hooks et CLI Le pointeur .active-slug enregistré
SKRAFT_TRACKING_ROOT Répertoire absolu contenant l’état de chaque pipeline .copilot-tracking/skraft-plans sous le répertoire de travail
SKRAFT_AUDIT_LOG Fichier du journal d’audit skraft/skill-audit.jsonl dans le répertoire git du projet, sinon logs/ du plugin
SKRAFT_CONFIG Config du framework (skraft-framework.config.json) Celle livrée avec le runtime
SKRAFT_CONFIG_ROOT Répertoire de la config de dépôt skraft-config.json Le répertoire de travail
SKRAFT_HARNESS Force le dialecte du payload (claude-code ou copilot) Déduit du payload

src/application/config-loader.mjs implémente une cascade env → ~/.skraft/config.json → .skraftrc.json, mais aucun hook ni CLI ne la lit.

Fichiers source

Fichier Rôle
plugins/skraft-framework/hooks/hooks.json Manifeste de hooks canonique
plugins/skraft-framework/com.github.copilot/hooks/hooks.json Copie générée pour Copilot v1
plugins/skraft-framework/src/cli/hook.mjs Point d’entrée CLI (stdin → stdout)
plugins/skraft-framework/src/cli/housekeeping.mjs Point d’entrée de SessionStart
plugins/skraft-framework/src/cli/state.mjs CLI d’état, porte de phase, bloc de handoff et timeline compris
plugins/skraft-framework/src/adapters/api/hooks/harness-input.mjs Payload harness → payload du framework
plugins/skraft-framework/src/adapters/api/hooks/payload.mjs Normalisation payload
plugins/skraft-framework/src/adapters/api/hooks/decision.mjs Constructeurs de décision (vocabulaire interne)
plugins/skraft-framework/src/adapters/api/hooks/harness-output.mjs Décision → format de fil harness
plugins/skraft-framework/src/adapters/api/hooks/hook-router.mjs Routage par type d’événement
plugins/skraft-framework/src/application/pre-tool-use-composite.mjs G1, provenance et G7/G8/G9 sur PreToolUse
plugins/skraft-framework/src/application/handoff-guard-service.mjs Garde de handoff G9
plugins/skraft-framework/src/domain/pipeline-policy.mjs Ordre de dispatch, provenance, continuation
plugins/skraft-framework/src/domain/handoff-policy.mjs Manifeste de handoff des entrées obligatoires et évaluation G9
plugins/skraft-framework/src/domain/session-guard-policy.mjs Protection de l’état suivi et écritures de l’orchestrateur dans l’espace de travail
plugins/skraft-framework/src/domain/skill-policy.mjs Politique des skills obligatoires et on-demand
plugins/skraft-framework/src/domain/phase-gate-policy.mjs Règles de clôture de phase
plugins/skraft-framework/src/adapters/infrastructure/jsonl-audit-writer.mjs Audit append-only

Voir aussi