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.askse résout au texte tapé. Le hook le compare avecRun it, donc n'importe quel autre texte refuse la commande. - Personne ne répond :
$.ui.askrejette quand l'utilisateur rejette la question ou choisit Chat about this, et dans une exécutionclaude -p, donc le bloccatchlaisse 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 :
- La garde intégrée
sec-default@builtin, un mod intégré à Claude Code que/pluginénumère en tant quecc-plugin-sec-default, où il charge, les mods que votre organisation énumère dansprependPlugins, puis tout autre mod qui compte comme celui de votre organisation et n'est pas dansappendPlugins - Les mods que vous installez
- Les mods que votre organisation énumère dans
appendPlugins - 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
PreToolUsedes paramètres gérés : s'exécutent avant le hooktool.calldu premier mod, et un bloc de l'un d'eux est final, donc aucun mod ne voit l'appel. - Hooks
PreToolUsede chaque autre fichier de paramètres et deshooks/hooks.jsondes plugins : s'exécutent après que le dernier mod appellenext, comme partie du comportement propre de Claude Code. Un mod qui répond àtool.callsans appelernextles empêche de s'exécuter, et un mod qui appellenextvoit 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
nextse 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