Skip to main content
Skip to content

Travailler avec les hooks

Les hooks vous permettent de connecter une logique personnalisée à chaque étape d’une session Copilot, à partir du moment où elle démarre, par le biais de chaque invite utilisateur et appel d’outil, jusqu’au moment où elle se termine. Ce guide décrit les cas d’usage pratiques afin de pouvoir expédier des autorisations, des audits, des notifications et bien plus encore sans modifier le comportement de l’agent principal.

Overview

Un hook est un rappel que vous inscrivez une fois lors de la création d’une session. Le Kit de développement logiciel (SDK) l’appelle à un point bien défini dans le cycle de vie de la conversation, transmet une entrée contextuelle et accepte éventuellement la sortie qui modifie le comportement de la session.

Diagramme : Organigramme montrant le processus décrit.

HookQuand il se déclencheCe que vous pouvez faire
Hooks du cycle de vie de la sessionLa session commence (nouvelle ou reprise)Injecter le contexte, les préférences de chargement
Hook déclenché à la soumission d’un prompt utilisateurL’utilisateur envoie un messageRéécrire les invites, ajouter du contexte, filtrer les entrées
Hook transformé par l’utilisateurRuntime génère l’invite de modèleExaminer ou remplacer le contenu destiné au modèle
Hook exécuté avant l’utilisation d’un outilAvant qu'un outil ne s'exécuteAutoriser/ refuser / modifier l’appel
Hook exécuté après l’utilisation d’un outilAprès le retour d'un outil (réussite uniquement)Transformation des résultats, rédiger des secrets, auditer
Hook exécuté après l’utilisation d’un outilAprès qu’un outil a renvoyé une erreurAjouter des instructions de nouvelle tentative, journaliser les échecs
Hooks du cycle de vie de la sessionFin de sessionNettoyer, enregistrer les métriques
Hook de gestion des erreursUne erreur est généréeJournalisation personnalisée, logique de nouvelle tentative, alertes

Tous les crochets sont facultatifs : inscrivez uniquement ceux dont vous avez besoin. Le renvoi null (ou l’équivalent du langage) à partir de n’importe quel hook indique au SDK de continuer avec le comportement par défaut.

Les hooks de dépôt basés sur des fichiers et les paramètres de hook de dépôt sont chargés uniquement une fois que l’espace de travail est de confiance. Les hooks d’utilisateur restent disponibles avant l’octroi de la confiance de l’espace de travail.

Hooks de dépôt dans des répertoires supplémentaires

Les hooks de système de fichiers du référentiel sont distincts des rappels du SDK décrits dans ce guide. Pour les charger à partir de répertoires supplémentaires lors de la création de session locale ou reprise à froid, fournissez additionalDirectories ; l’interface CLI --add-dir participe également par défaut.

DISABLE_ADDITIONAL_DIRECTORY_HOOKS=true restaure l’étendue principale uniquement ; enableFileHooks peut activer ou désactiver les hooks du dépôt via session.rpc.options.update; les répertoires de hooks explicites du SDK et les callbacks sont indépendants.

La découverte utilise la racine Git qui contient chaque répertoire accepté, ou le répertoire lui-même lorsqu’il se trouve en dehors d’un dépôt Git. Par exemple, fournir B\packages\core à l’intérieur du dépôt B prend en compte B\.github\hooks et les paramètres de hook du dépôt B, pas seulement la configuration à l’intérieur de B\packages\core.

Avertissement

Fournissez uniquement des répertoires de démarrage de confiance. Cela autorise les commandes de hook et les prompts fournis par hook à partir de tous les répertoires de démarrage éligibles sans approbation supplémentaire par répertoire ou par hook. Les autorisations d’accès aux fichiers accordées ultérieurement n’autorisent pas les hooks. La stratégie gérée existante, les contrôles de désactivation des hooks et les règles de bac à sable s’appliquent toujours ; ils ne rendent pas sûr le contenu de hook malveillant.

Les autorisations de répertoire ultérieures n’ajoutent pas de sources de hook. Une reprise à froid doit réapprovisionner les répertoires ; les mises à jour ultérieures des autorisations accordées ou des mécanismes de contournement ne peuvent pas modifier l’autorisation conservée.

Enregistrement des hooks

Les rappels hooks restent séparés des éléments adossés à un fichier hookDirectories. Pour connaître les configurations de hook empaquetées et les ressources adjacentes, consultez Emplacements de détection de la personnalisation.

Hooks déclaratifs à portée de l’agent

Les agents personnalisés peuvent déclarer un mappage d’événements hooks dans le frontmatter .agent.md ou dans la configuration de l’agent personnalisé du Kit de développement logiciel (SDK). Il s’agit de commandes déclaratives ou d’actions HTTP, et non de fonctions de rappel sdk. Ils s’exécutent uniquement pendant que cet agent est sélectionné, en plus des hooks de session, d’utilisateur, de dépôt, de plug-in et de stratégie managée applicables. La modification ou l’effacement de l’agent sélectionné modifie les hooks de portée pour l’appel suivant sans modifier d’autres sessions ou sous-agents concurrents.

---
name: formatter
description: Formats changed files
hooks:
  PostToolUse:
    - type: command
      command: ./scripts/format-changed-files.sh
      windows: ./scripts/format-changed-files.ps1
---
Format the files you change.

Les deux noms d’événements runtime, tels que postToolUse, ainsi que les alias VS Code pris en charge, tels que PostToolUse, sont acceptés. command fournit la commande de l’interpréteur de commandes de secours ; windows, linux et osx le remplacent sur la plateforme correspondante. Les commandes de hook sont relatives à la racine du référentiel de la session en cours d’exécution, sauf si elles définissent un répertoire de travail explicite. Les charges utiles des hooks et les décisions suivent les contrats de hook existants du runtime.

Lorsqu’un agent local s’exécute en tant que sous-agent, ses actions délimitées Stop (agentStop) s’exécutent à SubagentStop au lieu de s’exécuter à l’arrêt de tour ordinaire. Une action explicite SubagentStop sur cet agent continue d’observer ses propres enfants. Arrêt de la réception des rappels stop_hook_active: true après un bloc précédent. Après huit continuations d’arrêt délimitées consécutives, le runtime ignore d’autres blocs délimités et accepte la réponse finale de l’enfant, sauf si un hook de cycle de vie parent bloque toujours l’achèvement.

Les hooks de référentiel et d’agent hérité nécessitent l’autorisation d’accès à l’espace de travail. La découverte ou la sélection d’un agent n’accorde pas en soi de confiance. Les paramètres de désactivation du hook existant, les restrictions d’entreprise, les autorisations de bac à sable et les délais d’expiration s’appliquent également aux actions à portée limitée. Les agents fournis par le SDK ne peuvent pas revendiquer de privilèges de plugin géré. Les définitions d’agent hébergées à distance ou par l’organisation n’exécutent pas de hooks de portée limitée, et la distribution Stop de portée limitée est prise en charge pour les sous-agents d’exécution locaux, et non pour les participants AHP externes.

Les noms d’événements non pris en charge sont signalés avant la sélection de l’événement. Les erreurs de préparation d’action affectent uniquement leur événement déclaré ; les hooks de stratégie partagée et managée s’exécutent toujours.

Transmettez un hooks objet lorsque vous créez (ou reprenez) une session. Chaque exemple ci-dessous suit ce modèle.

Langages de code navigation

TypeScript
import { CopilotClient } from "@github/copilot-sdk";

const client = new CopilotClient();
await client.start();

const session = await client.createSession({
  hooks: {
    onSessionStart: async (input, invocation) => {
      /* ... */
    },
    onPreToolUse: async (input, invocation) => {
      /* ... */
    },
    onPostToolUse: async (input, invocation) => {
      /* ... */
    },
    // ... add only the hooks you need
  },
  onPermissionRequest: async () => ({ kind: "approve-once" }),
});

Conseil

Chaque gestionnaire de hook reçoit un paramètre invocation contenant le sessionId, ce qui est utile pour corréler les journaux et maintenir l’état par session.

Cas d’usage : contrôle d’autorisation

Permet onPreToolUse de générer une couche d’autorisation qui détermine les outils que l’agent peut exécuter, quels arguments sont autorisés et si l’utilisateur doit être invité avant l’exécution.

Autoriser une série d’outils sûrs

Langages de code navigation

TypeScript
const READ_ONLY_TOOLS = ["read_file", "glob", "grep", "view"];

const session = await client.createSession({
  hooks: {
    onPreToolUse: async (input) => {
      if (!READ_ONLY_TOOLS.includes(input.toolName)) {
        return {
          permissionDecision: "deny",
          permissionDecisionReason: `Only read-only tools are allowed. "${input.toolName}" was blocked.`,
        };
      }
      return { permissionDecision: "allow" };
    },
  },
  onPermissionRequest: async () => ({ kind: "approve-once" }),
});

Restreindre l’accès aux fichiers à des répertoires spécifiques

const ALLOWED_DIRS = ["/home/user/projects", "/tmp"];

const session = await client.createSession({
  hooks: {
    onPreToolUse: async (input) => {
      if (["read_file", "write_file", "edit"].includes(input.toolName)) {
        const filePath = (input.toolArgs as { path: string }).path;
        const allowed = ALLOWED_DIRS.some((dir) => filePath.startsWith(dir));

        if (!allowed) {
          return {
            permissionDecision: "deny",
            permissionDecisionReason: `Access to "${filePath}" is outside the allowed directories.`,
          };
        }
      }
      return { permissionDecision: "allow" };
    },
  },
  onPermissionRequest: async () => ({ kind: "approve-once" }),
});

Demander à l’utilisateur avant les opérations destructrices

const DESTRUCTIVE_TOOLS = ["delete_file", "shell", "bash"];

const session = await client.createSession({
  hooks: {
    onPreToolUse: async (input) => {
      if (DESTRUCTIVE_TOOLS.includes(input.toolName)) {
        return { permissionDecision: "ask" };
      }
      return { permissionDecision: "allow" };
    },
  },
  onPermissionRequest: async () => ({ kind: "approve-once" }),
});

Renvoyer "ask" délègue la décision à l’utilisateur lors de l’exécution — utile pour les actions destructrices où vous souhaitez qu’un humain intervienne.

Cas d’usage : audit et conformité

Combinez onPreToolUse,onPostToolUse , et les hooks du cycle de vie de la session pour créer une piste d’audit complète qui enregistre chaque action effectuée par l’agent.

Journal d’audit structuré

Langages de code navigation

TypeScript
interface AuditEntry {
  timestamp: Date;
  sessionId: string;
  event: string;
  toolName?: string;
  toolArgs?: unknown;
  toolResult?: unknown;
  prompt?: string;
}

const auditLog: AuditEntry[] = [];

const session = await client.createSession({
  hooks: {
    onSessionStart: async (input, invocation) => {
      auditLog.push({
        timestamp: input.timestamp,
        sessionId: invocation.sessionId,
        event: "session_start",
      });
      return null;
    },
    onUserPromptSubmitted: async (input, invocation) => {
      auditLog.push({
        timestamp: input.timestamp,
        sessionId: invocation.sessionId,
        event: "user_prompt",
        prompt: input.prompt,
      });
      return null;
    },
    onPreToolUse: async (input, invocation) => {
      auditLog.push({
        timestamp: input.timestamp,
        sessionId: invocation.sessionId,
        event: "tool_call",
        toolName: input.toolName,
        toolArgs: input.toolArgs,
      });
      return { permissionDecision: "allow" };
    },
    onPostToolUse: async (input, invocation) => {
      auditLog.push({
        timestamp: input.timestamp,
        sessionId: invocation.sessionId,
        event: "tool_result",
        toolName: input.toolName,
        toolResult: input.toolResult,
      });
      return null;
    },
    onSessionEnd: async (input, invocation) => {
      auditLog.push({
        timestamp: input.timestamp,
        sessionId: invocation.sessionId,
        event: "session_end",
      });

      // Persist the log — swap this with your own storage backend
      await fs.promises.writeFile(
        `audit-${invocation.sessionId}.json`,
        JSON.stringify(auditLog, null, 2),
      );
      return null;
    },
  },
  onPermissionRequest: async () => ({ kind: "approve-once" }),
});

Rédiger les secrets des résultats d’outil

const SECRET_PATTERNS = [
  /(?:api[_-]?key|token|secret|password)\s*[:=]\s*["']?[\w\-\.]+["']?/gi,
];

const session = await client.createSession({
  hooks: {
    onPostToolUse: async (input) => {
      if (typeof input.toolResult !== "string") return null;

      let redacted = input.toolResult;
      for (const pattern of SECRET_PATTERNS) {
        redacted = redacted.replace(pattern, "[REDACTED]");
      }

      return redacted !== input.toolResult
        ? { modifiedResult: redacted }
        : null;
    },
  },
  onPermissionRequest: async () => ({ kind: "approve-once" }),
});

Cas d’usage : notifications et sons

Les hooks se déclenchent dans le processus de votre application, ce qui vous permet de déclencher n’importe quel effet secondaire : notifications de bureau, sons, messages Slack ou appels webhook.

Notification de bureau sur les événements de session

Langages de code navigation

TypeScript
import notifier from "node-notifier"; // npm install node-notifier

const session = await client.createSession({
  hooks: {
    onSessionEnd: async (input, invocation) => {
      notifier.notify({
        title: "Copilot Session Complete",
        message: `Session ${invocation.sessionId.slice(0, 8)} finished (${input.reason}).`,
      });
      return null;
    },
    onErrorOccurred: async (input) => {
      notifier.notify({
        title: "Copilot Error",
        message: input.error.slice(0, 200),
      });
      return null;
    },
  },
  onPermissionRequest: async () => ({ kind: "approve-once" }),
});

Jouer un son lorsque l’exécution d’un outil est terminée.

import { exec } from "node:child_process";

const session = await client.createSession({
  hooks: {
    onPostToolUse: async (input) => {
      // macOS: play a system sound after every tool call
      exec("afplay /System/Library/Sounds/Pop.aiff");
      return null;
    },
    onErrorOccurred: async () => {
      exec("afplay /System/Library/Sounds/Basso.aiff");
      return null;
    },
  },
  onPermissionRequest: async () => ({ kind: "approve-once" }),
});

Publier sur Slack en cas d'erreurs

const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL!;

const session = await client.createSession({
  hooks: {
    onErrorOccurred: async (input, invocation) => {
      if (!input.recoverable) {
        await fetch(SLACK_WEBHOOK_URL, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            text: `🚨 Unrecoverable error in session \`${invocation.sessionId.slice(0, 8)}\`:\n\`\`\`${input.error}\`\`\``,
          }),
        });
      }
      return null;
    },
  },
  onPermissionRequest: async () => ({ kind: "approve-once" }),
});

Cas d’usage : enrichissement rapide

Utilisez onSessionStart et onUserPromptSubmitted injectez automatiquement le contexte afin que les utilisateurs n’aient pas à se répéter.

Injecter des métadonnées de projet au début de la session

const session = await client.createSession({
  hooks: {
    onSessionStart: async (input) => {
      const pkg = JSON.parse(
        await fs.promises.readFile("package.json", "utf-8"),
      );
      return {
        additionalContext: [
          `Project: ${pkg.name} v${pkg.version}`,
          `Node: ${process.version}`,
          `Working directory: ${input.workingDirectory}`,
        ].join("\n"),
      };
    },
  },
  onPermissionRequest: async () => ({ kind: "approve-once" }),
});

Développer les commandes abrégées dans les invites

const SHORTCUTS: Record<string, string> = {
  "/fix": "Find and fix all errors in the current file",
  "/test": "Write comprehensive unit tests for this code",
  "/explain": "Explain this code in detail",
  "/refactor": "Refactor this code to improve readability",
};

const session = await client.createSession({
  hooks: {
    onUserPromptSubmitted: async (input) => {
      for (const [shortcut, expansion] of Object.entries(SHORTCUTS)) {
        if (input.prompt.startsWith(shortcut)) {
          const rest = input.prompt.slice(shortcut.length).trim();
          return { modifiedPrompt: rest ? `${expansion}: ${rest}` : expansion };
        }
      }
      return null;
    },
  },
  onPermissionRequest: async () => ({ kind: "approve-once" }),
});

Cas d’usage : gestion des erreurs et récupération

Le onErrorOccurred crochet vous donne la possibilité de réagir aux défaillances, qu'il s'agisse de réessayer, d'avertir un humain ou d'arrêter de manière élégante.

Reessayer les erreurs de modèle transitoires

const session = await client.createSession({
  hooks: {
    onErrorOccurred: async (input) => {
      if (input.errorContext === "model_call" && input.recoverable) {
        return {
          errorHandling: "retry",
          retryCount: 3,
          userNotification: "Temporary model issue — retrying…",
        };
      }
      return null;
    },
  },
  onPermissionRequest: async () => ({ kind: "approve-once" }),
});

Messages d’erreur conviviaux

const FRIENDLY_MESSAGES: Record<string, string> = {
  model_call: "The AI model is temporarily unavailable. Please try again.",
  tool_execution: "A tool encountered an error. Check inputs and try again.",
  system: "A system error occurred. Please try again later.",
};

const session = await client.createSession({
  hooks: {
    onErrorOccurred: async (input) => {
      return {
        userNotification: FRIENDLY_MESSAGES[input.errorContext] ?? input.error,
      };
    },
  },
  onPermissionRequest: async () => ({ kind: "approve-once" }),
});

Cas d’usage : métriques de session

Suivez la durée d’exécution des sessions, le nombre d’outils appelés et la raison pour laquelle les sessions se terminent, utiles pour les tableaux de bord et la surveillance des coûts.

Langages de code navigation

TypeScript
const metrics = new Map<
  string,
  { start: Date; toolCalls: number; prompts: number }
>();

const session = await client.createSession({
  hooks: {
    onSessionStart: async (input, invocation) => {
      metrics.set(invocation.sessionId, {
        start: input.timestamp,
        toolCalls: 0,
        prompts: 0,
      });
      return null;
    },
    onUserPromptSubmitted: async (_input, invocation) => {
      metrics.get(invocation.sessionId)!.prompts++;
      return null;
    },
    onPreToolUse: async (_input, invocation) => {
      metrics.get(invocation.sessionId)!.toolCalls++;
      return { permissionDecision: "allow" };
    },
    onSessionEnd: async (input, invocation) => {
      const m = metrics.get(invocation.sessionId)!;
      const durationSec =
        (input.timestamp.getTime() - m.start.getTime()) / 1000;

      console.log(
        `Session ${invocation.sessionId.slice(0, 8)}: ` +
          `${durationSec.toFixed(1)}s, ${m.prompts} prompts, ` +
          `${m.toolCalls} tool calls, ended: ${input.reason}`,
      );

      metrics.delete(invocation.sessionId);
      return null;
    },
  },
  onPermissionRequest: async () => ({ kind: "approve-once" }),
});

Combinaison de crochets

Les hooks se composent naturellement. Un seul hooks objet peut gérer les autorisations et l’audit et les notifications : chaque hook effectue son propre travail.

const session = await client.createSession({
  hooks: {
    onSessionStart: async (input) => {
      console.log(`[audit] session started in ${input.workingDirectory}`);
      return { additionalContext: "Project uses TypeScript and Vitest." };
    },
    onPreToolUse: async (input) => {
      console.log(`[audit] tool requested: ${input.toolName}`);
      if (input.toolName === "shell") {
        return { permissionDecision: "ask" };
      }
      return { permissionDecision: "allow" };
    },
    onPostToolUse: async (input) => {
      console.log(`[audit] tool completed: ${input.toolName}`);
      return null;
    },
    onErrorOccurred: async (input) => {
      console.error(`[alert] ${input.errorContext}: ${input.error}`);
      return null;
    },
    onSessionEnd: async (input, invocation) => {
      console.log(
        `[audit] session ${invocation.sessionId.slice(0, 8)} ended: ${input.reason}`,
      );
      return null;
    },
  },
  onPermissionRequest: async () => ({ kind: "approve-once" }),
});

Bonnes pratiques

  1. Maintenez les hooks rapides. Chaque hook s’exécute en ligne — les hooks lents retardent la conversation. Déchargez le travail lourd (écritures de base de données, appels HTTP) dans une file d’attente en arrière-plan si possible.

  2. Retournez null quand vous n’avez rien à changer. Cela indique au Kit de développement logiciel (SDK) de continuer avec les valeurs par défaut et d’éviter l’allocation inutile d’objets.

  3. Soyez explicite avec les décisions d’autorisation. Le retour { permissionDecision: "allow" } est plus clair que le retour null, même si les deux autorisent l’outil.

  4. N’avalez pas les erreurs critiques. Il est judicieux de supprimer les erreurs d’outil récupérables, mais toujours journaliser ou alerter sur des erreurs irrécupérables.

  5. Utilisez additionalContext plutôt modifiedPrompt que si possible. L’ajout de contexte conserve l’intention d’origine de l’utilisateur tout en guidant le modèle.

  6. État du scope par ID de session. Si vous suivez les données par session, associez-les à invocation.sessionId et nettoyez-les dans onSessionEnd.

Reference

Pour obtenir des définitions de type complètes, des tables de champs d’entrée/sortie et des exemples supplémentaires pour chaque hook, consultez la référence de l’API :

Voir aussi