Reaccionar a eventos con un mod
Maneja eventos de Claude Code desde un mod: observa, reescribe o responde llamadas de herramientas, indicaciones y turnos, filtra qué eventos maneja un hook y planifica para otros mods.
Un hook es un manejador de eventos: una función que Claude Code ejecuta cuando ocurre un evento nombrado. Claude Code dispara un evento en cada punto donde está a punto de actuar, como cuando ejecuta una herramienta, envía una indicación, envía una solicitud al modelo, o inicia o termina una sesión. Tu hook se ejecuta antes de que Claude Code actúe, por lo que puede observar el evento, reescribirlo o responderlo en lugar de Claude Code. Registras un hook con on(eventName, handler).
Construye tu primer mod antes de empezar aquí. Para cada evento y sus campos exactos, consulta la referencia o lee los tipos para tu compilación.
Cómo un hook maneja un evento
Un hook se sitúa entre un evento y lo que Claude Code haría al respecto, por lo que puede observar el evento, reescribirlo o responderlo él mismo. Recibe tres argumentos: la API de mods como $, el evento como e, y el siguiente manejador como next. Los manejadores de un evento forman una cadena de middleware. next(e) llama al siguiente manejador, que es el hook de otro mod o, al final de la cadena, el comportamiento propio de Claude Code, y se resuelve al resultado. Lo que tu hook hace con next decide cuál de los tres hace.
Observar un evento
Para observar un evento sin cambiarlo, haz tu trabajo y devuelve next(e). Este hook registra cada herramienta que Claude está a punto de usar:
on('tool.call', async ($, e, next) => {
// Se ejecuta antes de que la herramienta se ejecute
$.ui.log('Claude is about to use ' + e.tool)
// Pasa el evento sin cambios
return next(e)
})
Antes de que cada herramienta se ejecute, aparece una línea atenuada como ● my-mod: Claude is about to use Bash en la transcripción, donde my-mod es el nombre de tu plugin. La herramienta se ejecuta como lo haría sin el mod.
Para actuar después del evento, await next(e), haz tu trabajo y devuelve el resultado. Este hook registra cada herramienta después de que se ha ejecutado:
on('tool.call', async ($, e, next) => {
// Deja que la herramienta se ejecute y espera su resultado
const result = await next(e)
// Se ejecuta después de que la herramienta se ejecuta
$.ui.log(e.tool + ' finished')
// Devuelve el resultado sin cambios
return result
})
La línea ahora aparece después de que cada herramienta termina. Claude lee el mismo resultado de cualquier manera, porque el hook devuelve lo que next(e) se resolvió.
Reescribir un evento
Para cambiar lo que Claude Code actúa, como el texto de una indicación, llama a next con una copia modificada del evento. El evento en sí es inmutable: está congelado en cada profundidad, y asignar a un campo lanza una excepción. Este hook recorta cada indicación antes de que se envíe:
on('prompt.submit', async ($, e, next) => {
// Pasa una copia del evento con su texto cambiado
return next({ ...e, text: e.text.trim() })
})
Los manejadores posteriores y Claude Code reciben la indicación recortada y nunca ven la original. También puedes cambiar el resultado: await next(e), luego devuelve una copia del resultado con un campo reemplazado.
Responder un evento
Para manejar un evento tú mismo, devuelve un resultado sin llamar a next. Eso cortocircuita la cadena, por lo que los mods posteriores y el comportamiento propio de Claude Code no se ejecutan. Este hook rechaza cada comando Bash:
on('tool.call', { tool: 'Bash' }, async () => {
// Sin llamada a next, por lo que el comando nunca se ejecuta
return { deny: 'Bash is turned off in this project. Use the file tools.' }
})
Cuando Claude intenta un comando Bash, el comando no se ejecuta y Claude lee el texto deny como el resultado de la herramienta. Cada evento tiene su propia forma de resultado, que la referencia de eventos enumera.
Filtrar qué eventos maneja un hook
Para ejecutar un hook solo para algunos eventos, pasa un filtro como segundo argumento a on. Claude Code llama al filtro un matcher. Es un objeto cuyos campos se comparan con los del evento, y el hook se ejecuta solo cuando cada campo coincide. Un campo puede ser un valor, una matriz de valores permitidos o una expresión regular.
Cada línea en este ejemplo registra la misma función, hook, para un conjunto más estrecho de llamadas de herramientas:
// Una cadena coincide con un valor: solo llamadas Bash
on('tool.call', { tool: 'Bash' }, hook)
// Una matriz coincide con cualquier valor en ella: llamadas Edit y Write
on('tool.call', { tool: ['Edit', 'Write'] }, hook)
// Una expresión regular coincide por patrón: cada herramienta de un servidor MCP
on('tool.call', { tool: /^mcp__github__/ }, hook)
hook se ejecuta una vez para una llamada Bash, Edit o Write, y una vez para una llamada a una herramienta cuyo nombre comienza con mcp__github__. Una llamada a cualquier otra herramienta, como Read, no coincide con ninguna de las tres, por lo que hook no se ejecuta para ella.
El nombre del evento puede ser un comodín. 'classic.*' coincide con cada evento de hook de configuración. '*' coincide con cada evento excepto los eventos de telemetría, que enganchas por nombre o como 'telemetry.*'.
Registra cada evento una vez por matcher. Si llamas a on dos veces para session.start sin un matcher, el módulo falla al cargar con on("session.start") is registered twice without a matcher. Pon todo lo que tu mod hace al inicio de la sesión en un hook.
Enganchar lo que Claude está haciendo
Engancha estos eventos para ver o cambiar una llamada de herramienta, una indicación o un turno mientras sucede. Para cada evento y lo que un hook puede devolver, consulta la referencia de eventos.
Guardar o cambiar una llamada de herramienta
Un hook tool.call ve cada herramienta que Claude está a punto de usar, por lo que puede rechazar la llamada, cambiar sus argumentos o dejarla pasar. tool.call se dispara cuando Claude Code está a punto de ejecutar una herramienta, incluidas las llamadas que hacen los subagentes y las llamadas a herramientas MCP. e.tool es el nombre de la herramienta y los argumentos de la herramienta son campos de e, como e.command para Bash. Cuando llamas a next(e), Claude Code ejecuta la verificación de permisos y luego la herramienta.
Este hook rechaza un comando Bash que hace un push forzado y le dice a Claude por qué:
// El matcher limita el hook a llamadas Bash, por lo que e.command es el comando del shell
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
if (/git push .*--force/.test(e.command)) {
// Devolver sin llamar a next responde el evento, por lo que el comando nunca se ejecuta
return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
}
// Cada otro comando va a la verificación de permisos y luego a Bash
return next(e)
})
Cuando Claude intenta git push --force, el comando no se ejecuta y no aparece ningún aviso de permiso, porque el hook nunca llama a next. Claude lee el texto deny como el resultado de la herramienta, así que escríbelo como una instrucción en la que Claude pueda actuar. Cada otro comando Bash se ejecuta como lo haría sin el mod.
Para actuar después de que una herramienta se ha ejecutado, await next(e), haz tu trabajo y devuelve lo que next te dio. Este hook registra cada archivo .mdx que Claude cambia, con $.ui.log, que añade una línea atenuada a la transcripción que Claude no lee:
on('tool.call', { tool: ['Edit', 'Write'] }, async ($, e, next) => {
// Espera la verificación de permisos y la herramienta, y mantén lo que produjeron
const result = await next(e)
// Una llamada rechazada vuelve como { deny }, y una fallida tiene isError establecido
const changed = !result.deny && !result.isError
if (changed && e.file_path.endsWith('.mdx')) $.ui.log('Claude changed ' + e.file_path)
// Devuelve el resultado tal como vino, para que Claude lea lo que la herramienta devolvió
return result
})
Después de que Claude edita o escribe un archivo .mdx, una línea atenuada en la transcripción nombra el archivo. Nada se registra para otro tipo de archivo, o para una llamada que fue rechazada o falló. La vista de Claude de la llamada no cambia, porque el hook devuelve el resultado que recibió.
Para cambiar una llamada, pasa argumentos cambiados a next. Para reintentar una llamada, llama a next(e) de nuevo: un hook que ve isError en el primer resultado puede ejecutar la herramienta una segunda vez y devolver ese resultado. Para responder una llamada tú mismo, devuelve un objeto con un campo result, como { result: 'Skipped by my-mod' }, sin llamar a next. Cuando haces eso, no aparece ningún aviso de permiso y la herramienta no se ejecuta, por lo que el resultado que devuelves es todo lo que Claude aprende sobre lo que sucedió.
Los hooks en la configuración administrada de tu organización se ejecutan antes que el hook tool.call de cualquier mod, y un bloqueo de uno de ellos es final.
Mantener una llamada de herramienta hasta que el usuario decida
Un hook puede pausar una llamada de herramienta y preguntarle al usuario qué hacer antes de que continúe. Un hook tool.call puede await antes de llamar a next o devolver, y la llamada de herramienta permanece pendiente hasta entonces. Para hacer la pregunta al usuario, llama a $.ui.ask. Muestra tu pregunta encima de una lista numerada de tus opciones, en el diálogo que Claude usa para preguntarte algo, y se resuelve a la etiqueta que el usuario elige. Después de tus opciones, el diálogo añade una fila para escribir una respuesta diferente y una fila Chat about this.
El patrón RISKY en este ejemplo coincide con rm -r, rm -rf, git reset --hard y git push con --force, y se pierde otras ortografías como git push -f. Este módulo pregunta antes de ejecutar un comando Bash que coincida con el patrón:
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) => {
// Deja pasar cada otro comando sin una pregunta
if (!RISKY.test(e.command)) return next(e)
// Comienza desde la respuesta segura, por lo que una pregunta que nadie responde rechaza el comando
let answer = 'Refuse'
try {
// La llamada de herramienta espera aquí hasta que el usuario elige una de las dos etiquetas
answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
} catch {
// El usuario descartó la pregunta, o esto es una ejecución de claude -p sin nadie a quien preguntar
}
if (answer !== 'Run it') {
// Responde sin llamar a next, por lo que el comando no se ejecuta
return { deny: 'The user declined this command. Ask before trying a different approach.' }
}
return next(e)
})
}
Cuando Claude intenta un comando como rm -rf build, la pregunta aparece con el comando en ella, y el comando espera la respuesta:
- El usuario elige Run it: el hook llama a
next(e), y la verificación de permisos habitual aún se ejecuta después - El usuario elige Refuse: el comando no se ejecuta y Claude lee el texto
deny - El usuario escribe una respuesta:
$.ui.askse resuelve al texto escrito. El hook lo compara conRun it, por lo que cualquier otro texto rechaza el comando. - Nadie responde:
$.ui.askrechaza cuando el usuario descarta la pregunta o elige Chat about this, y en una ejecución declaude -p, por lo que el bloquecatchdeja la respuesta enRefuse
Mantén la espera dentro de una llamada de API de mods como $.ui.ask, porque ese tiempo no cuenta contra el límite de tiempo de 10 segundos del hook. El tiempo dedicado a esperar una promesa propia sí cuenta. Claude Code omite un hook que agota el tiempo, por lo que el comando retenido se ejecutaría.
Reescribir o añadir a una indicación
Un hook prompt.submit ve cada indicación antes de que comience el turno, por lo que puede reescribir el texto o añadirle. e.text es lo que se escribió.
| Para hacer esto | Devuelve esto |
|---|---|
| Reescribe la indicación. El mensaje en la transcripción muestra el nuevo texto. | next({ ...e, text: newText }) |
| Añade texto que solo Claude lee, después de la indicación | next({ ...e, context: [...(e.context ?? []), extraText] }) |
| Detén el envío de la indicación | { drop: 'the reason' } |
Este hook añade el nombre de la rama actual para Claude siempre que una indicación menciona una solicitud de extracción:
on('prompt.submit', async ($, e, next) => {
// Pasa una indicación que no menciona una solicitud de extracción tal como está
if (!/\bPR\b|pull request/i.test(e.text)) return next(e)
const git = await $.process.run(['git', 'branch', '--show-current'])
// Fuera de un repositorio git el comando falla, por lo que no hay rama para añadir
if (git.exitCode !== 0) return next(e)
// Mantén cualquier contexto que un hook anterior añadió, y añade una línea más para Claude
return next({ ...e, context: [...(e.context ?? []), 'Current branch: ' + git.stdout.trim()] })
})
Cuando envías una indicación como open a PR for this change, tu mensaje se ve igual en la transcripción, y Claude también lee una línea como Current branch: feature/auth después de ella. Una indicación que no menciona una solicitud de extracción pasa sin cambios, y git no se ejecuta.
Otros eventos cubren el resto de lo que Claude lee: prompt.section para cada sección de la indicación del sistema, prompt.context para el contexto enviado con el primer mensaje, y skill.prompt para el texto de una skill. El texto de estos hooks que cambia entre solicitudes invalida el caché de indicaciones.
Seguir un turno
Un turno es todo lo que Claude hace en respuesta a una indicación. Engancha turn.start, turn.step y turn.complete para seguir uno:
| Evento | Cuándo se dispara | Qué puede hacer un hook |
|---|---|---|
turn.start |
Comienza un turno | Observa. e.turnId identifica el turno en los otros dos eventos. |
turn.step |
Claude Code está a punto de enviar una solicitud al modelo. Un turno con llamadas de herramientas tiene varias. e.agentId se establece para la solicitud de un subagente. |
Lee el uso de tokens de cada solicitud, envíalo a un modelo diferente con next({ ...e, model }), o responde sin llamar al modelo |
turn.complete |
El turno terminó, incluido un turno que el usuario interrumpió, donde e.isAborted es true. e.answer es el texto final de Claude, e.durationMs cuánto tiempo tomó, y e.usage los totales de tokens del turno. Un turno de subagente lo dispara con e.agentId establecido. |
Observa, o devuelve un objeto con un campo text, como { text: 'Done in 12 seconds' }, para mostrar una línea bajo la respuesta |
Escribe un hook turn.step como un generador asincrónico, porque el evento transmite. yield* next(e) reenvía la respuesta mientras se transmite y se evalúa al resultado terminado. Este hook registra cuánto de cada solicitud sirvió la API de Claude desde el caché de indicaciones:
// function* hace que el hook sea un generador, que puede pasar la respuesta pieza por pieza
on('turn.step', async function* ($, e, next) {
// Envía la solicitud, reenvía cada pieza mientras llega, y mantén el resultado terminado
const result = yield* next(e)
// Omite un resultado que no reporta conteos de tokens
if (result.usage) {
$.ui.log('cache read ' + result.usage.cache_read_input_tokens + ' · wrote ' + result.usage.cache_creation_input_tokens)
}
// Devuelve el resultado sin cambios, para que el turno continúe como de costumbre
return result
})
La respuesta de Claude se transmite a la pantalla como lo haría sin el mod. Después de que cada solicitud termina, una línea atenuada en la transcripción da el número de tokens leídos del caché y el número escrito en él. Un turno con llamadas de herramientas tiene varias solicitudes, por lo que añade varias líneas.
result.usage contiene los cuatro conteos de tokens que la API de Claude reporta para una solicitud, más el model que respondió: input_tokens, output_tokens, cache_read_input_tokens y cache_creation_input_tokens. El hook se ejecuta para las solicitudes de subagentes también, por lo que verifica e.agentId cuando quieres solo la conversación principal.
Enganchar los eventos de hook de configuración
Los hooks de configuración son los hooks de comando, HTTP, indicación y agente que configuras en archivos de configuración. Cada evento de hook de configuración, como Stop, SessionEnd o PostToolUse, es también un evento nombrado classic. seguido del nombre del evento de hook de configuración, como classic.Stop. e es el JSON que un hook de configuración recibe en stdin, incluido transcript_path.
Este hook usa Stop, que se dispara cuando Claude termina de responder, para registrar dónde se guarda la transcripción de la sesión:
on('classic.Stop', async ($, e, next) => {
// e tiene los mismos campos que un hook Stop en un archivo de configuración lee de stdin
$.ui.log('Transcript saved at ' + e.transcript_path)
// Pasa el evento, para que los hooks Stop en tus archivos de configuración aún se ejecuten
return next(e)
})
Cada vez que Claude termina de responder, una línea atenuada en la transcripción da la ruta del archivo de transcripción. El hook devuelve next(e), por lo que observa el evento y no cambia nada sobre cómo termina el turno.
Ejecutar junto a otros mods
Varios mods pueden enganchar el mismo evento, y cualquiera de ellos puede fallar. Si tu mod bloquea llamadas de herramientas, verifica su posición en la cadena y qué sucede cuando su hook falla.
El orden en que se ejecutan los mods
Los hooks en el mismo evento forman una cadena de middleware. Cada next de un mod llama al hook del siguiente mod, y el último next llega al comportamiento propio de Claude Code. El primer mod es el más externo: ve el evento antes que los otros y el resultado después de ellos, y decide si los otros se ejecutan en absoluto. Un mod posterior no puede detener a uno anterior de ver un evento.
Claude Code ordena la cadena por dónde viene cada mod:
- El guardia incorporado
sec-default@builtin, un mod incorporado en Claude Code que/pluginenumera comocc-plugin-sec-default, donde se carga, mods que tu organización enumera enprependPlugins, y luego cualquier otro mod que cuente como de tu organización y no esté enappendPlugins - Mods que instalas
- Mods que tu organización enumera en
appendPlugins - Otros mods incorporados en Claude Code
Entre los mods que instalas, un mod se ejecuta antes que los mods que enumera bajo dependencies en su manifiesto. Dentro de un módulo, los hooks se ejecutan en el orden en que register llamó a on.
Dónde se ejecutan los hooks de configuración en el orden
Los hooks PreToolUse configurados en archivos de configuración también se ejecutan durante una llamada de herramienta, en puntos fijos en la cadena de mods:
- Hooks
PreToolUsede configuración administrada: se ejecutan antes que el hooktool.calldel primer mod, y un bloqueo de uno de ellos es final, por lo que ningún mod ve la llamada. - Hooks
PreToolUsede cada otro archivo de configuración y dehooks/hooks.jsonde plugins: se ejecutan después de que el último mod llama anext, como parte del comportamiento propio de Claude Code. Un mod que respondetool.callsin llamar anextlos mantiene de ejecutarse, y un mod que llama anextve su decisión en el resultado que devuelve.
tool.check es el evento donde Claude Code decide si una llamada de herramienta puede ejecutarse. Se dispara después de esos hooks y las reglas de permisos han decidido, y next(e) se resuelve a su decisión. Un hook en tool.check puede devolver una decisión diferente, como { decision: 'allow' }, por lo que puede aprobar una llamada que un hook en el segundo grupo bloqueó. Extender permisos con hooks enumera qué decisiones se mantienen sobre un mod.
Manejar un hook que falla
Un hook que falla no rompe la sesión, y puedes decidir qué sucede en su lugar. Cuando un hook sin un manejador .catch lanza una excepción, agota el tiempo o devuelve un resultado de forma incorrecta, lo que sucede después depende de si había llamado a next:
- Falló antes de llamar a
next: Claude Code lo omite, y el siguiente manejador se ejecuta en su lugar - Falló después de que
nextse resolvió: ese resultado se mantiene, y nada se ejecuta una segunda vez
Una línea nombra el mod, el evento y la razón, como my-mod: tool.call hook skipped: threw Error: boom. Dónde lo lees depende de la sesión, como Averigua por qué un mod no hace nada enumera. Un hook ui.render cuyo dibujo no valida se reporta de manera diferente, como Construir un árbol a partir de elementos describe.
Para hacer que un hook que bloquea llamadas falle cerrado, añade un manejador de error .catch que responda en su lugar. Aquí, guard es tu función de hook:
// on devuelve un registro, y .catch adjunta un manejador a ese hook
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
// next.error.kind es 'throw' o 'timeout', que dice cómo falló guard
return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})
Mientras guard funciona, el manejador nunca se ejecuta. Cuando guard lanza una excepción o agota el tiempo en una llamada Bash, Claude Code llama al manejador con el mismo evento. El manejador devuelve { deny }, por lo que el comando no se ejecuta, y Claude lee el texto con throw o timeout al final. Sin el manejador, Claude Code omitería guard y ejecutaría el comando. El manejador tiene un segundo para responder.
Próximos pasos
- Usa la API de mods: añade comandos y herramientas, llama a un modelo y ejecuta trabajo en un temporizador
- Dibuja en la interfaz: muestra lo que tus hooks recopilan en un panel o encima de la indicación
- Prueba un mod: dispara cualquiera de estos eventos desde una prueba
- Referencia de mods: cada evento, cada método de API de mods y los límites