SpyBara
Go Premium

plugins/mods/events.md 2026-10-01 23:59 UTC to 2026-10-02 11:59 UTC

This page contains 90 additions and 63 deletions.

2026
Thu 1 23:59 Fri 2 13:00

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'un prompt, 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 prompt avant qu'il ne soit envoyé :

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, qui prennent leur propre nom et un filtre { to: 'collector' }.

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.

Intercepter ce que fait Claude

Gérez ces événements pour observer ou modifier un appel d'outil, un prompt ou un tour au moment où il se produit. Pour connaître tous les événements et ce qu'un hook peut renvoyer, consultez la référence des événements.

Contrôler ou modifier un appel d'outil

Un hook tool.call voit chaque outil que Claude s'apprête à utiliser, ce qui lui permet de refuser l'appel, de modifier ses arguments ou de le laisser passer. tool.call se déclenche lorsque Claude Code s'apprête à exécuter un outil, y compris pour les appels effectués par un sous-agent 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. Lorsque vous appelez next(e), Claude Code effectue la vérification des permissions, puis exécute l'outil.

Ce hook refuse une commande Bash qui effectue un force-push et indique à Claude pourquoi :

// The matcher limits the hook to Bash calls, so e.command is the shell command
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
  if (/git push .*--force/.test(e.command)) {
    // Returning without calling next answers the event, so the command never runs
    return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
  }
  // Every other command goes on to the permission check and then to Bash
  return next(e)
})

Lorsque Claude tente git push --force, la commande ne s'exécute pas et aucune demande de permission n'apparaît, car le hook n'appelle jamais next. Claude lit le texte de deny comme résultat de l'outil ; rédigez-le donc comme une instruction sur laquelle Claude peut agir. Toutes les autres commandes Bash s'exécutent comme elles le feraient sans le mod.

Pour agir après l'exécution d'un outil, faites await next(e), effectuez votre traitement, puis renvoyez ce que next vous a fourni. Ce hook consigne chaque fichier .mdx que Claude modifie, avec $.ui.log, qui ajoute à la transcription une ligne estompée que Claude ne lit pas :

on('tool.call', { tool: ['Edit', 'Write'] }, async ($, e, next) => {
  // Wait for the permission check and the tool, and keep what they produced
  const result = await next(e)
  // A refused call comes back as { deny }, and a failed one has isError set
  const changed = !result.deny && !result.isError
  if (changed && e.file_path.endsWith('.mdx')) $.ui.log('Claude changed ' + e.file_path)
  // Return the result as it came, so Claude reads what the tool returned
  return result
})

Après que Claude a modifié ou écrit un fichier .mdx, une ligne estompée dans la transcription indique le nom du fichier. Rien n'est consigné pour un autre type de fichier, ni pour un appel refusé ou en échec. La vision que Claude a de l'appel ne change pas, car le hook renvoie le résultat qu'il a reçu.

Pour modifier un appel, transmettez des arguments modifiés à next. Pour réessayer un appel, appelez de nouveau next(e) : un hook qui voit isError dans le premier résultat peut exécuter l'outil une seconde fois et renvoyer ce résultat. Pour répondre vous-même à un appel, renvoyez un objet avec un champ result, comme { result: 'Skipped by my-mod' }, sans appeler next. Dans ce cas, aucune demande de permission n'apparaît et l'outil ne s'exécute pas ; le résultat que vous renvoyez est donc tout ce que Claude apprend de ce qui s'est passé.

Les hooks des paramètres gérés de votre organisation s'exécutent avant le hook tool.call de tout mod, et un blocage provenant de l'un d'eux est définitif.

Suspendre un appel d'outil jusqu'à la décision de l'utilisateur

Un hook peut mettre en pause un appel d'outil et demander à l'utilisateur quoi faire avant de poursuivre. Un hook tool.call peut faire un await avant d'appeler next ou de renvoyer une valeur, et l'appel d'outil reste en attente jusque-là. Pour poser la question à l'utilisateur, appelez $.ui.ask. Cette fonction 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 demander quelque chose, et se résout avec le libellé choisi par l'utilisateur. Après vos options, la boîte de dialogue ajoute une ligne pour saisir une autre réponse et une ligne Chat about this.

Le motif RISKY de cet exemple correspond à rm -r, rm -rf, git reset --hard et git push avec --force, et ne détecte pas d'autres écritures comme git push -f. Ce module pose une question 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) => {
    // Let every other command through without a question
    if (!RISKY.test(e.command)) return next(e)
    // Start from the safe answer, so a question nobody answers refuses the command
    let answer = 'Refuse'
    try {
      // The tool call waits here until the user picks one of the two labels
      answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
    } catch {
      // The user dismissed the question, or this is a claude -p run with nobody to ask
    }
    if (answer !== 'Run it') {
      // Answer without calling next, so the command doesn't run
      return { deny: 'The user declined this command. Ask before trying a different approach.' }
    }
    return next(e)
  })
}

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

  • L'utilisateur choisit Run it : le hook appelle next(e), et la vérification habituelle des permissions s'exécute toujours ensuite
  • L'utilisateur choisit Refuse : la commande ne s'exécute pas, et Claude lit le texte de deny
  • L'utilisateur saisit une réponse : $.ui.ask se résout avec le texte saisi. Le hook le compare à Run it, donc tout autre texte refuse la commande.
  • Personne ne répond : $.ui.ask est rejetée lorsque l'utilisateur ferme la question ou choisit Chat about this, ainsi que lors d'une exécution claude -p ; le bloc catch laisse donc la réponse à Refuse

Placez l'attente dans un appel à l'API des mods comme $.ui.ask, car ce temps n'est pas décompté de la limite de temps du hook. Le temps passé à attendre une promesse de votre propre code, en revanche, est décompté. Claude Code ignore un hook qui dépasse le délai, si bien que la commande suspendue s'exécuterait.

Approuver ou refuser un appel d'outil avant que l'utilisateur ne soit sollicité

Pour décider si un appel d'outil peut s'exécuter, gérez tool.check, l'événement où Claude Code prend cette décision. Il se déclenche après que les règles de permission et les hooks des paramètres ont décidé, et next(e) se résout avec leur décision : allow, ask ou deny. Votre hook renvoie cette décision ou une autre. e.input contient les arguments de l'outil, comme command pour Bash.

Pour une commande ou un chemin fixe, utilisez une règle de permission comme Bash(npm test), qui ne nécessite aucun code. Gérez tool.check lorsque la décision dépend de ce qui est vrai à ce moment-là, comme la branche Git courante ou une valeur enregistrée par un autre hook.

Ce hook refuse git push lorsque la branche courante est main :

on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
  // What the permission rules and settings hooks decided: 'allow', 'ask', or 'deny'
  const decided = await next(e)
  if (!e.input.command.includes('git push')) return decided
  const branch = await $.process.run(['git', 'branch', '--show-current'])
  if (branch.stdout.trim() !== 'main') return decided
  return { decision: 'deny', reason: 'Push from a branch other than main' }
})

Sur main, le hook renvoie deny, même lorsqu'une règle autorise git push. Sur une autre branche, et pour les autres commandes, l'appel obtient la décision qu'il obtiendrait sans le mod.

Le hook se base sur le texte de la commande ; considérez-le donc comme un garde-fou pour Claude. Pour bloquer les push vers main pour tout le monde, protégez la branche sur votre hébergeur Git.

Un hook peut renvoyer allow, ask ou deny ; il peut donc aussi approuver un appel qu'un hook PreToolUse situé en dehors des paramètres gérés a bloqué. Étendre les permissions avec des hooks indique quelles décisions prévalent sur un mod.

Réécrire un prompt ou y ajouter du contenu

Un hook prompt.submit voit chaque prompt avant le début du tour, ce qui lui permet de réécrire le texte ou d'y ajouter du contenu. e.text correspond à ce qui a été saisi.

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

Ce hook ajoute pour Claude le nom de la branche courante chaque fois qu'un prompt mentionne une pull request :

on('prompt.submit', async ($, e, next) => {
  // Pass on a prompt that doesn't mention a pull request as it is
  if (!/\bPR\b|pull request/i.test(e.text)) return next(e)
  const git = await $.process.run(['git', 'branch', '--show-current'])
  // Outside a git repository the command fails, so there's no branch to add
  if (git.exitCode !== 0) return next(e)
  // Keep any context an earlier hook added, and add one more line for Claude
  return next({ ...e, context: [...(e.context ?? []), 'Current branch: ' + git.stdout.trim()] })
})

Lorsque vous envoyez un prompt comme open a PR for this change, votre message reste identique dans la transcription, et Claude lit également après celui-ci une ligne comme Current branch: feature/auth. Un prompt qui ne mentionne pas de pull request passe sans modification, et git ne s'exécute pas.

D'autres événements couvrent le reste de ce que lit Claude : prompt.section pour chaque section du prompt système, prompt.context pour le contexte envoyé avec le premier message, et skill.prompt pour le texte d'un skill. Un texte issu de ces hooks qui change d'une requête à l'autre invalide le cache des prompts.

Suivre un tour

Un tour correspond à tout ce que fait Claude en réponse à un prompt. Gérez 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 comportant des appels d'outils en compte plusieurs. e.agentId est défini pour la requête d'un sous-agent. Lire l'utilisation des tokens de chaque requête, l'envoyer à un autre modèle avec next({ ...e, model }), ou répondre sans appeler le modèle
turn.complete Le tour s'est terminé, y compris un tour interrompu par l'utilisateur, auquel cas e.isAborted vaut true. e.answer est le texte final de Claude, e.durationMs sa durée, et e.usage le total des tokens du tour. Le tour d'un sous-agent le déclenche avec e.agentId défini. Observer, ou renvoyer 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 sous forme de générateur asynchrone, car l'événement est diffusé en streaming. yield* next(e) transmet la réponse au fil du streaming et s'évalue en résultat final. Ce hook consigne la part de chaque requête que l'API Claude a servie depuis le cache des prompts :

// function* makes the hook a generator, which can pass the response on piece by piece
on('turn.step', async function* ($, e, next) {
  // Send the request, forward each piece as it arrives, and keep the finished result
  const result = yield* next(e)
  // Skip a result that reports no token counts
  if (result.usage) {
    $.ui.log('cache read ' + result.usage.cache_read_input_tokens + ' · wrote ' + result.usage.cache_creation_input_tokens)
  }
  // Return the result unchanged, so the turn continues as usual
  return result
})

La réponse de Claude s'affiche en streaming à l'écran comme sans le mod. Une fois chaque requête terminée, une ligne estompée dans la transcription indique le nombre de tokens lus depuis le cache et le nombre de tokens écrits dans celui-ci. Un tour comportant des appels d'outils contient plusieurs requêtes ; il ajoute donc plusieurs lignes.

result.usage contient le nombre de tokens que l'API Claude indique pour une requête, ainsi que 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 ; vérifiez donc e.agentId si vous ne souhaitez traiter que la conversation principale.

Gérer les événements des hooks des paramètres

Les hooks des paramètres sont les hooks de type commande, HTTP, prompt et agent que vous configurez dans les fichiers de paramètres. Chaque événement de hook des paramètres, comme Stop, SessionEnd ou PostToolUse, est aussi un événement nommé classic. suivi du nom de l'événement, 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 lorsque Claude a fini de répondre, pour consigner l'emplacement où la transcription de la session est enregistrée :

on('classic.Stop', async ($, e, next) => {
  // e has the same fields a Stop hook in a settings file reads from stdin
  $.ui.log('Transcript saved at ' + e.transcript_path)
  // Pass the event on, so Stop hooks in your settings files still run
  return next(e)
})

Chaque fois que Claude a fini de répondre, une ligne estompée dans la transcription indique le chemin du fichier de transcription. Le hook renvoie next(e) : il observe donc l'événement sans rien changer à la façon dont le tour se termine.

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

Plusieurs mods peuvent gérer 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 se déclenche une fois que ces hooks et les règles de permission ont décidé, de sorte qu'un hook sur cet événement peut approuver un appel qu'un hook du deuxième groupe a bloqué.

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 sa propre limite de temps, plus courte.

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