SpyBara
Go Premium

plugins/mods/events.md 2026-09-30 23:00 UTC to 2026-10-01 21:02 UTC

This page contains 336 additions and 0 deletions.

2026
Thu 1 21:02

Réagir aux événements avec un mod

Gérez les événements Claude Code à partir d'un mod : observez, réécrivez ou répondez aux appels d'outils, aux invites et aux tours, filtrez les événements qu'un hook gère, et planifiez pour d'autres mods.

Un hook est un gestionnaire d'événements : une fonction que Claude Code exécute quand un événement nommé se produit. Claude Code déclenche un événement à chaque point où il s'apprête à agir, par exemple quand il exécute un outil, soumet une invite, envoie une requête au modèle, ou démarre ou termine une session. Votre hook s'exécute avant que Claude Code n'agisse, il peut donc observer l'événement, le réécrire, ou y répondre à la place de Claude Code. Vous enregistrez un hook avec on(eventName, handler).

Construisez votre premier mod avant de commencer ici. Pour chaque événement et ses champs exacts, consultez la référence ou lisez les types pour votre build.

Comment un hook gère un événement

Un hook se situe entre un événement et ce que Claude Code ferait à ce sujet, il peut donc observer l'événement, le réécrire, ou y répondre lui-même. Il reçoit trois arguments : l'API mods en tant que $, l'événement en tant que e, et le gestionnaire suivant en tant que next. Les gestionnaires d'un événement forment une chaîne middleware. next(e) appelle le gestionnaire suivant, qui est le hook d'un autre mod ou, à la fin de la chaîne, le comportement propre de Claude Code, et il se résout au résultat. Ce que votre hook fait avec next décide lequel des trois il fait.

Observer un événement

Pour observer un événement sans le modifier, faites votre travail et retournez next(e). Ce hook enregistre chaque outil que Claude s'apprête à utiliser :

on('tool.call', async ($, e, next) => {
  // S'exécute avant que l'outil ne s'exécute
  $.ui.log('Claude is about to use ' + e.tool)
  // Passer l'événement inchangé
  return next(e)
})

Avant chaque exécution d'outil, une ligne atténuée telle que ● my-mod: Claude is about to use Bash apparaît dans la transcription, où my-mod est le nom de votre plugin. L'outil s'exécute comme il le ferait sans le mod.

Pour agir après l'événement, await next(e), faites votre travail, et retournez le résultat. Ce hook enregistre chaque outil après son exécution :

on('tool.call', async ($, e, next) => {
  // Laisser l'outil s'exécuter et attendre son résultat
  const result = await next(e)
  // S'exécute après que l'outil s'exécute
  $.ui.log(e.tool + ' finished')
  // Retourner le résultat inchangé
  return result
})

La ligne apparaît maintenant après chaque fin d'outil. Claude lit le même résultat de toute façon, car le hook retourne ce que next(e) s'est résolu à.

Réécrire un événement

Pour modifier ce sur quoi Claude Code agit, comme le texte d'une invite, appelez next avec une copie modifiée de l'événement. L'événement lui-même est immuable : il est gelé à chaque profondeur, et l'assignation à un champ lève une exception. Ce hook supprime les espaces de chaque invite avant qu'elle ne soit envoyée :

on('prompt.submit', async ($, e, next) => {
  // Passer une copie de l'événement avec son texte modifié
  return next({ ...e, text: e.text.trim() })
})

Les gestionnaires ultérieurs et Claude Code reçoivent l'invite supprimée et ne voient jamais l'original. Vous pouvez également modifier le résultat : await next(e), puis retourner une copie du résultat avec un champ remplacé.

Répondre à un événement

Pour gérer un événement vous-même, retournez un résultat sans appeler next. Cela court-circuite la chaîne, donc les mods ultérieurs et le comportement propre de Claude Code ne s'exécutent pas. Ce hook refuse chaque commande Bash :

on('tool.call', { tool: 'Bash' }, async () => {
  // Pas d'appel à next, donc la commande ne s'exécute jamais
  return { deny: 'Bash is turned off in this project. Use the file tools.' }
})

Quand Claude essaie une commande Bash, la commande ne s'exécute pas, et Claude lit le texte deny comme le résultat de l'outil. Chaque événement a sa propre forme de résultat, que la référence des événements énumère.

Filtrer les événements qu'un hook gère

Pour exécuter un hook pour certains événements seulement, passez un filtre comme deuxième argument à on. Claude Code appelle le filtre un matcher. C'est un objet dont les champs sont comparés avec ceux de l'événement, et le hook s'exécute seulement quand chaque champ correspond. Un champ peut être une valeur, un tableau de valeurs autorisées, ou une expression régulière.

Chaque ligne dans cet exemple enregistre la même fonction, hook, pour un ensemble plus restreint d'appels d'outils :

// Une chaîne correspond à une valeur : appels Bash seulement
on('tool.call', { tool: 'Bash' }, hook)
// Un tableau correspond à n'importe quelle valeur dedans : appels Edit et Write
on('tool.call', { tool: ['Edit', 'Write'] }, hook)
// Une expression régulière correspond par motif : chaque outil d'un serveur MCP
on('tool.call', { tool: /^mcp__github__/ }, hook)

hook s'exécute une fois pour un appel Bash, Edit ou Write, et une fois pour un appel à un outil dont le nom commence par mcp__github__. Un appel à n'importe quel autre outil, comme Read, ne correspond à aucun des trois, donc hook ne s'exécute pas pour lui.

Le nom de l'événement peut être un wildcard. 'classic.*' correspond à chaque événement hook des paramètres. '*' correspond à chaque événement sauf les événements de télémétrie, que vous hookez par nom ou en tant que 'telemetry.*'.

Enregistrez chaque événement une fois par matcher. Si vous appelez on deux fois pour session.start sans matcher, le module échoue à charger avec on("session.start") is registered twice without a matcher. Mettez tout ce que votre mod fait au démarrage de la session dans un hook.

Hook ce que Claude fait

Hookez ces événements pour voir ou modifier un appel d'outil, une invite, ou un tour au moment où cela se produit. Pour chaque événement et ce qu'un hook peut retourner, consultez la référence des événements.

Garder ou modifier un appel d'outil

Un hook tool.call voit chaque outil que Claude s'apprête à utiliser, il peut donc refuser l'appel, modifier ses arguments, ou le laisser passer. tool.call se déclenche quand Claude Code s'apprête à exécuter un outil, y compris les appels qu'un sous-agent fait et les appels aux outils MCP. e.tool est le nom de l'outil et les arguments de l'outil sont des champs de e, comme e.command pour Bash. Quand vous appelez next(e), Claude Code exécute la vérification des permissions puis l'outil.

Ce hook refuse une commande Bash qui force-push, et dit à Claude pourquoi :

// Le matcher limite le hook aux appels Bash, donc e.command est la commande shell
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
  if (/git push .*--force/.test(e.command)) {
    // Retourner sans appeler next répond à l'événement, donc la commande ne s'exécute jamais
    return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
  }
  // Chaque autre commande va à la vérification des permissions puis à Bash
  return next(e)
})

Quand Claude essaie git push --force, la commande ne s'exécute pas et aucune invite de permission n'apparaît, car le hook n'appelle jamais next. Claude lit le texte deny comme le résultat de l'outil, donc écrivez-le comme une instruction sur laquelle Claude peut agir. Chaque autre commande Bash s'exécute comme elle le ferait sans le mod.

Pour agir après l'exécution d'un outil, await next(e), faites votre travail, et retournez ce que next vous a donné. Ce hook enregistre chaque fichier .mdx que Claude modifie, avec $.ui.log, qui ajoute une ligne atténuée à la transcription que Claude ne lit pas :

on('tool.call', { tool: ['Edit', 'Write'] }, async ($, e, next) => {
  // Attendre la vérification des permissions et l'outil, et garder ce qu'ils ont produit
  const result = await next(e)
  // Un appel refusé revient en tant que { deny }, et un appel échoué a isError défini
  const changed = !result.deny && !result.isError
  if (changed && e.file_path.endsWith('.mdx')) $.ui.log('Claude changed ' + e.file_path)
  // Retourner le résultat tel qu'il est venu, donc Claude lit ce que l'outil a retourné
  return result
})

Après que Claude édite ou écrit un fichier .mdx, une ligne atténuée dans la transcription nomme le fichier. Rien n'est enregistré pour un autre type de fichier, ou pour un appel qui a été refusé ou échoué. La vue de Claude de l'appel ne change pas, car le hook retourne le résultat qu'il a reçu.

Pour modifier un appel, passez des arguments modifiés à next. Pour réessayer un appel, appelez next(e) à nouveau : un hook qui voit isError sur le premier résultat peut exécuter l'outil une deuxième fois et retourner ce résultat. Pour répondre à un appel vous-même, retournez un objet avec un champ result, comme { result: 'Skipped by my-mod' }, sans appeler next. Quand vous faites cela, aucune invite de permission n'apparaît et l'outil ne s'exécute pas, donc le résultat que vous retournez est tout ce que Claude apprend sur ce qui s'est passé.

Les hooks dans les paramètres gérés de votre organisation s'exécutent avant le hook tool.call de n'importe quel mod, et un bloc de l'un d'eux est final.

Tenir un appel d'outil jusqu'à ce que l'utilisateur décide

Un hook peut mettre en pause un appel d'outil et demander à l'utilisateur quoi faire avant qu'il ne continue. Un hook tool.call peut await avant d'appeler next ou de retourner, et l'appel d'outil reste en attente jusqu'à ce moment. Pour poser la question à l'utilisateur, appelez $.ui.ask. Il affiche votre question au-dessus d'une liste numérotée de vos options, dans la boîte de dialogue que Claude utilise pour vous poser une question, et se résout à l'étiquette que l'utilisateur choisit. Après vos options, la boîte de dialogue ajoute une ligne pour taper une réponse différente et une ligne Chat about this.

Le motif RISKY dans cet exemple correspond à rm -r, rm -rf, git reset --hard, et git push avec --force, et il manque d'autres orthographes comme git push -f. Ce module demande avant d'exécuter une commande Bash qui correspond au motif :

const RISKY = /\brm\s+-rf?\b|\bgit\s+reset\s+--hard\b|\bgit\s+push\b.*--force/

export function register(on) {
  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
    // Laisser chaque autre commande passer sans question
    if (!RISKY.test(e.command)) return next(e)
    // Commencer par la réponse sûre, donc une question à laquelle personne ne répond refuse la commande
    let answer = 'Refuse'
    try {
      // L'appel d'outil attend ici jusqu'à ce que l'utilisateur choisisse l'une des deux étiquettes
      answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
    } catch {
      // L'utilisateur a rejeté la question, ou c'est une exécution claude -p sans personne à demander
    }
    if (answer !== 'Run it') {
      // Répondre sans appeler next, donc la commande ne s'exécute pas
      return { deny: 'The user declined this command. Ask before trying a different approach.' }
    }
    return next(e)
  })
}

Quand Claude essaie une commande comme rm -rf build, la question apparaît avec la commande dedans, et la commande attend la réponse :

  • L'utilisateur choisit Run it : le hook appelle next(e), et la vérification des permissions habituelle s'exécute toujours après
  • L'utilisateur choisit Refuse : la commande ne s'exécute pas, et Claude lit le texte deny
  • L'utilisateur tape une réponse : $.ui.ask se résout au texte tapé. Le hook le compare avec Run it, donc n'importe quel autre texte refuse la commande.
  • Personne ne répond : $.ui.ask rejette quand l'utilisateur rejette la question ou choisit Chat about this, et dans une exécution claude -p, donc le bloc catch laisse la réponse à Refuse

Gardez l'attente à l'intérieur d'un appel API mods comme $.ui.ask, car ce temps ne compte pas contre la limite de temps de 10 secondes du hook. Le temps passé à attendre une promesse de votre propre compte. Claude Code saute un hook qui expire, donc la commande tenue s'exécuterait.

Réécrire ou ajouter à une invite

Un hook prompt.submit voit chaque invite avant le début du tour, il peut donc réécrire le texte ou l'ajouter. e.text est ce qui a été tapé.

Pour faire ceci Retournez ceci
Réécrire l'invite. Le message dans la transcription affiche le nouveau texte. next({ ...e, text: newText })
Ajouter du texte que seul Claude lit, après l'invite next({ ...e, context: [...(e.context ?? []), extraText] })
Empêcher l'invite d'être envoyée { drop: 'the reason' }

Ce hook ajoute le nom de la branche actuelle pour Claude chaque fois qu'une invite mentionne une pull request :

on('prompt.submit', async ($, e, next) => {
  // Passer une invite qui ne mentionne pas une pull request telle qu'elle est
  if (!/\bPR\b|pull request/i.test(e.text)) return next(e)
  const git = await $.process.run(['git', 'branch', '--show-current'])
  // En dehors d'un référentiel git, la commande échoue, donc il n'y a pas de branche à ajouter
  if (git.exitCode !== 0) return next(e)
  // Garder tout contexte qu'un hook antérieur a ajouté, et en ajouter une ligne de plus pour Claude
  return next({ ...e, context: [...(e.context ?? []), 'Current branch: ' + git.stdout.trim()] })
})

Quand vous envoyez une invite comme open a PR for this change, votre message ressemble au même dans la transcription, et Claude lit aussi une ligne comme Current branch: feature/auth après. Une invite qui ne mentionne pas une pull request passe inchangée, et git ne s'exécute pas.

D'autres événements couvrent le reste de ce que Claude lit : prompt.section pour chaque section de l'invite système, prompt.context pour le contexte envoyé avec le premier message, et skill.prompt pour le texte d'une compétence. Le texte de ces hooks qui change entre les requêtes invalide le cache d'invite.

Suivre un tour

Un tour est tout ce que Claude fait en réponse à une invite. Hookez turn.start, turn.step, et turn.complete pour en suivre un :

Événement Quand il se déclenche Ce qu'un hook peut faire
turn.start Un tour commence Observer. e.turnId identifie le tour dans les deux autres événements.
turn.step Claude Code s'apprête à envoyer une requête au modèle. Un tour avec des appels d'outils en a plusieurs. e.agentId est défini pour la requête d'un sous-agent. Lire l'utilisation des jetons de chaque requête, l'envoyer à un modèle différent avec next({ ...e, model }), ou répondre sans appeler le modèle
turn.complete Le tour s'est terminé, y compris un tour que l'utilisateur a interrompu, où e.isAborted est true. e.answer est le texte final de Claude, e.durationMs combien de temps cela a pris, et e.usage les totaux de jetons du tour. Un tour d'un sous-agent le déclenche avec e.agentId défini. Observer, ou retourner un objet avec un champ text, comme { text: 'Done in 12 seconds' }, pour afficher une ligne sous la réponse

Écrivez un hook turn.step comme un générateur asynchrone, car l'événement diffuse. yield* next(e) transfère la réponse au fur et à mesure qu'elle diffuse et s'évalue au résultat terminé. Ce hook enregistre combien de chaque requête l'API Claude a servi à partir du cache d'invite :

// function* rend le hook un générateur, qui peut transférer la réponse morceau par morceau
on('turn.step', async function* ($, e, next) {
  // Envoyer la requête, transférer chaque morceau à son arrivée, et garder le résultat terminé
  const result = yield* next(e)
  // Sauter un résultat qui ne rapporte pas de comptes de jetons
  if (result.usage) {
    $.ui.log('cache read ' + result.usage.cache_read_input_tokens + ' · wrote ' + result.usage.cache_creation_input_tokens)
  }
  // Retourner le résultat inchangé, donc le tour continue comme d'habitude
  return result
})

La réponse de Claude diffuse à l'écran comme elle le ferait sans le mod. Après chaque fin de requête, une ligne atténuée dans la transcription donne le nombre de jetons lus du cache et le nombre écrit dedans. Un tour avec des appels d'outils a plusieurs requêtes, donc il ajoute plusieurs lignes.

result.usage contient les quatre comptes de jetons que l'API Claude rapporte pour une requête, plus le model qui a répondu : input_tokens, output_tokens, cache_read_input_tokens, et cache_creation_input_tokens. Le hook s'exécute aussi pour les requêtes des sous-agents, donc vérifiez e.agentId quand vous voulez seulement la conversation principale.

Hook les événements hook des paramètres

Les hooks des paramètres sont les hooks de commande, HTTP, d'invite et d'agent que vous configurez dans les fichiers de paramètres. Chaque événement hook des paramètres, comme Stop, SessionEnd, ou PostToolUse, est aussi un événement nommé classic. suivi du nom de l'événement hook des paramètres, comme classic.Stop. e est le JSON qu'un hook des paramètres reçoit sur stdin, y compris transcript_path.

Ce hook utilise Stop, qui se déclenche quand Claude finit de répondre, pour enregistrer où la transcription de la session est sauvegardée :

on('classic.Stop', async ($, e, next) => {
  // e a les mêmes champs qu'un hook Stop dans un fichier de paramètres lit depuis stdin
  $.ui.log('Transcript saved at ' + e.transcript_path)
  // Passer l'événement, donc les hooks Stop dans vos fichiers de paramètres s'exécutent toujours
  return next(e)
})

Chaque fois que Claude finit de répondre, une ligne atténuée dans la transcription donne le chemin du fichier de transcription. Le hook retourne next(e), il observe donc l'événement et ne change rien à la façon dont le tour se termine.

Exécuter aux côtés d'autres mods

Plusieurs mods peuvent hooker le même événement, et n'importe lequel d'eux peut échouer. Si votre mod bloque les appels d'outils, vérifiez sa position dans la chaîne et ce qui se passe quand son hook échoue.

L'ordre dans lequel les mods s'exécutent

Les hooks sur le même événement forment une chaîne middleware. Chaque next d'un mod appelle le hook du mod suivant, et le dernier next atteint le comportement propre de Claude Code. Le premier mod est le plus externe : il voit l'événement avant les autres et le résultat après eux, et il décide si les autres s'exécutent du tout. Un mod ultérieur ne peut pas empêcher un mod antérieur de voir un événement.

Claude Code ordonne la chaîne par où chaque mod vient :

  1. La garde intégrée sec-default@builtin, un mod intégré à Claude Code que /plugin énumère en tant que cc-plugin-sec-default, où il charge, les mods que votre organisation énumère dans prependPlugins, puis tout autre mod qui compte comme celui de votre organisation et n'est pas dans appendPlugins
  2. Les mods que vous installez
  3. Les mods que votre organisation énumère dans appendPlugins
  4. D'autres mods intégrés à Claude Code

Parmi les mods que vous installez, un mod s'exécute avant les mods qu'il énumère sous dependencies dans son manifeste. Dans un module, les hooks s'exécutent dans l'ordre que register a appelé on.

Où les hooks des paramètres s'exécutent dans l'ordre

Les hooks PreToolUse configurés dans les fichiers de paramètres s'exécutent aussi pendant un appel d'outil, à des points fixes dans la chaîne des mods :

  • Hooks PreToolUse des paramètres gérés : s'exécutent avant le hook tool.call du premier mod, et un bloc de l'un d'eux est final, donc aucun mod ne voit l'appel.
  • Hooks PreToolUse de chaque autre fichier de paramètres et des hooks/hooks.json des plugins : s'exécutent après que le dernier mod appelle next, comme partie du comportement propre de Claude Code. Un mod qui répond à tool.call sans appeler next les empêche de s'exécuter, et un mod qui appelle next voit leur décision dans le résultat qu'il retourne.

tool.check est l'événement où Claude Code décide si un appel d'outil peut s'exécuter. Il se déclenche après ces hooks et les règles de permission ont décidé, et next(e) se résout à leur décision. Un hook sur tool.check peut retourner une décision différente, comme { decision: 'allow' }, il peut donc approuver un appel qu'un hook du deuxième groupe a bloqué. Étendre les permissions avec des hooks énumère quelles décisions tiennent sur un mod.

Gérer un hook qui échoue

Un hook qui échoue ne casse pas la session, et vous pouvez décider ce qui se passe à la place. Quand un hook sans gestionnaire .catch lève une exception, expire, ou retourne un résultat de la mauvaise forme, ce qui se passe ensuite dépend de s'il avait appelé next :

  • Il a échoué avant d'appeler next : Claude Code le saute, et le gestionnaire suivant s'exécute à sa place
  • Il a échoué après que next se soit résolu : ce résultat tient, et rien ne s'exécute une deuxième fois

Une ligne nomme le mod, l'événement, et la raison, comme my-mod: tool.call hook skipped: threw Error: boom. Où vous la lisez dépend de la session, comme Découvrir pourquoi un mod ne fait rien l'énumère. Un hook ui.render dont le dessin ne valide pas est rapporté différemment, comme Construire un arbre à partir d'éléments le décrit.

Pour faire échouer un hook qui bloque les appels fermé, ajoutez un gestionnaire d'erreur .catch qui répond à sa place. Ici, guard est votre fonction hook :

// on retourne une enregistrement, et .catch attache un gestionnaire à ce hook seul
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
  // next.error.kind est 'throw' ou 'timeout', qui dit comment guard a échoué
  return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})

Pendant que guard fonctionne, le gestionnaire ne s'exécute jamais. Quand guard lève une exception ou expire sur un appel Bash, Claude Code appelle le gestionnaire avec le même événement. Le gestionnaire retourne { deny }, donc la commande ne s'exécute pas, et Claude lit le texte avec throw ou timeout à la fin. Sans le gestionnaire, Claude Code sauterait guard et exécuterait la commande. Le gestionnaire a une seconde pour répondre.

Prochaines étapes

  • Utiliser l'API mods : ajouter des commandes et des outils, appeler un modèle, et exécuter du travail sur un minuteur
  • Dessiner dans l'interface : afficher ce que vos hooks collectent dans un volet ou au-dessus de l'invite
  • Tester un mod : déclencher n'importe lequel de ces événements à partir d'un test
  • Référence des mods : chaque événement, chaque méthode API mods, et les limites