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 aquello sobre lo que actúa Claude Code, como el texto de un prompt, 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 prompt 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 usan su propio nombre y un filtro { to: 'collector' }.
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.
Intercepta lo que Claude está haciendo
Maneja estos eventos para ver o cambiar una llamada a herramienta, un prompt o un turno mientras ocurre. Para ver cada evento y lo que puede devolver un hook, consulta la referencia de eventos.
Protege o cambia una llamada a herramienta
Un hook tool.call ve cada herramienta que Claude está a punto de usar, así 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 hace un subagente 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 comprobación de permisos y luego la herramienta.
Este hook rechaza un comando de Bash que hace un push forzado y le dice a Claude por qué:
// 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)
})
Cuando Claude intenta git push --force, el comando no se ejecuta y no aparece ninguna solicitud de permiso, porque el hook nunca llama a next. Claude lee el texto de deny como el resultado de la herramienta, así que escríbelo como una instrucción sobre la que Claude pueda actuar. Cualquier otro comando de Bash se ejecuta como lo haría sin el mod.
Para actuar después de que una herramienta se haya ejecutado, usa 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 agrega a la transcripción una línea atenuada que Claude no lee:
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
})
Después de que Claude edita o escribe un archivo .mdx, una línea atenuada en la transcripción nombra el archivo. No se registra nada para otro tipo de archivo, ni para una llamada que fue rechazada o que falló. La vista que Claude tiene de la llamada no cambia, porque el hook devuelve el resultado que recibió.
Para cambiar una llamada, pasa los argumentos modificados a next. Para reintentar una llamada, vuelve a llamar a next(e): un hook que ve isError en el primer resultado puede ejecutar la herramienta una segunda vez y devolver ese resultado. Para responder tú mismo a una llamada, devuelve un objeto con un campo result, como { result: 'Skipped by my-mod' }, sin llamar a next. Cuando haces eso, no aparece ninguna solicitud de permiso y la herramienta no se ejecuta, así que el resultado que devuelves es todo lo que Claude sabe sobre lo que pasó.
Los hooks de 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 definitivo.
Retén una llamada a herramienta hasta que el usuario decida
Un hook puede pausar una llamada a herramienta y preguntarle al usuario qué hacer antes de que continúe. Un hook tool.call puede usar await antes de llamar a next o de devolver un valor, y la llamada a herramienta queda pendiente hasta entonces. Para plantearle la pregunta al usuario, llama a $.ui.ask. Muestra tu pregunta sobre una lista numerada de tus opciones, en el diálogo que Claude usa para preguntarte algo, y se resuelve con la etiqueta que elige el usuario. Después de tus opciones, el diálogo agrega una fila para escribir una respuesta diferente y una fila Chat about this.
El patrón RISKY de este ejemplo coincide con rm -r, rm -rf, git reset --hard y git push con --force, y no detecta otras formas de escribirlos, como git push -f. Este módulo pregunta antes de ejecutar un comando de Bash que coincide 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) => {
// 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)
})
}
Cuando Claude intenta un comando como rm -rf build, aparece la pregunta con el comando incluido, y el comando espera la respuesta:
- El usuario elige Run it: el hook llama a
next(e), y la comprobación de permisos habitual se sigue ejecutando después - El usuario elige Refuse: el comando no se ejecuta y Claude lee el texto de
deny - El usuario escribe una respuesta:
$.ui.askse resuelve con el texto escrito. El hook lo compara conRun it, así que cualquier otro texto rechaza el comando. - Nadie responde:
$.ui.askse rechaza cuando el usuario descarta la pregunta o elige Chat about this, y en una ejecución declaude -p, así que el bloquecatchdeja la respuesta enRefuse
Mantén la espera dentro de una llamada a la API de mods como $.ui.ask, porque ese tiempo no cuenta para el límite de tiempo del hook. El tiempo que pasas esperando una promesa propia sí cuenta. Claude Code omite un hook que excede el tiempo límite, así que el comando retenido se ejecutaría.
Aprueba o rechaza una llamada a herramienta antes de preguntarle al usuario
Para decidir si una llamada a herramienta puede ejecutarse, maneja tool.check, el evento en el que Claude Code toma esa decisión. Se dispara después de que las reglas de permisos y los hooks de configuración han decidido, y next(e) se resuelve con su decisión: allow, ask o deny. Tu hook devuelve esa decisión u otra diferente. e.input contiene los argumentos de la herramienta, como command para Bash.
Para un comando o una ruta fijos, usa una regla de permisos como Bash(npm test), que no requiere código. Maneja tool.check cuando la decisión depende de lo que es cierto en ese momento, como la rama de Git actual o un valor que registró otro hook.
Este hook rechaza git push mientras la rama actual es 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' }
})
En main, el hook devuelve deny, incluso cuando una regla permite git push. En otra rama, y para otros comandos, la llamada obtiene la decisión que obtendría sin el mod.
El hook compara el texto del comando, así que trátalo como un recordatorio para Claude. Para bloquear los pushes a main para todos, protege la rama en tu host de Git.
Un hook puede devolver allow, ask o deny, así que también puede aprobar una llamada que bloqueó un hook PreToolUse fuera de la configuración administrada. Amplía los permisos con hooks indica qué decisiones prevalecen sobre un mod.
Reescribe un prompt o agrégale contenido
Un hook prompt.submit ve cada prompt antes de que empiece el turno, así que puede reescribir el texto o agregarle contenido. e.text es lo que se escribió.
| Para hacer esto | Devuelve esto |
|---|---|
| Reescribir el prompt. El mensaje en la transcripción muestra el texto nuevo. | next({ ...e, text: newText }) |
| Agregar texto que solo Claude lee, después del prompt | next({ ...e, context: [...(e.context ?? []), extraText] }) |
| Impedir que se envíe el prompt | { drop: 'the reason' } |
Este hook agrega el nombre de la rama actual para Claude siempre que un prompt menciona un 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()] })
})
Cuando envías un prompt 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 él. Un prompt que no menciona un pull request pasa sin cambios, y git no se ejecuta.
Otros eventos cubren el resto de lo que Claude lee: prompt.section para cada sección del prompt del sistema, prompt.context para el contexto enviado con el primer mensaje y skill.prompt para el texto de un skill. El texto de estos hooks que cambia entre solicitudes invalida la caché de prompts.
Sigue un turno
Un turno es todo lo que Claude hace en respuesta a un prompt. Maneja turn.start, turn.step y turn.complete para seguir uno:
| Evento | Cuándo se dispara | Qué puede hacer un hook |
|---|---|---|
turn.start |
Empieza un turno | Observar. 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 a herramientas tiene varias. e.agentId se establece para la solicitud de un subagente. |
Leer el uso de tokens de cada solicitud, enviarla a un modelo diferente con next({ ...e, model }) o responder 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 tardó y e.usage los totales de tokens del turno. El turno de un subagente lo dispara con e.agentId establecido. |
Observar, o devolver un objeto con un campo text, como { text: 'Done in 12 seconds' }, para mostrar una línea debajo de la respuesta |
Escribe un hook turn.step como un generador asíncrono, porque el evento se transmite en streaming. yield* next(e) reenvía la respuesta a medida que se transmite y se evalúa como el resultado final. Este hook registra qué parte de cada solicitud sirvió la API de Claude desde la caché de 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 respuesta de Claude se transmite a la pantalla como lo hace sin el mod. Después de que termina cada solicitud, una línea atenuada en la transcripción indica el número de tokens leídos de la caché y el número escritos en ella. Un turno con llamadas a herramientas tiene varias solicitudes, así que agrega varias líneas.
result.usage contiene los recuentos de tokens que la API de Claude informa para una solicitud, además del model que respondió: input_tokens, output_tokens, cache_read_input_tokens y cache_creation_input_tokens. El hook también se ejecuta para las solicitudes de los subagentes, así que comprueba e.agentId cuando quieras solo la conversación principal.
Maneja los eventos de los hooks de configuración
Los hooks de configuración son los hooks de comando, HTTP, prompt y agente que configuras en los archivos de configuración. Cada evento de hook de configuración, como Stop, SessionEnd o PostToolUse, también es un evento llamado classic. seguido del nombre del evento del 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 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)
})
Cada vez que Claude termina de responder, una línea atenuada en la transcripción indica la ruta del archivo de transcripción. El hook devuelve next(e), así que observa el evento y no cambia nada de cómo termina el turno.
Ejecutar junto a otros mods
Varios mods pueden manejar el mismo evento, y cualquiera de ellos puede fallar. Si tu mod bloquea llamadas a 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 se dispara después de que esos hooks y las reglas de permisos han decidido, por lo que un hook en él puede aprobar una llamada que un hook del segundo grupo bloqueó.
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 de espera 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 omitiría guard y ejecutaría el comando. El manejador tiene su propio límite de tiempo, más corto.
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