SpyBara
Go Premium

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

This page contains 137 additions and 110 deletions.

2026
Thu 1 23:59 Fri 2 22:00

Reaccionar a eventos con un mod

Maneja eventos de Claude Code desde un mod: observa, reescribe o responde llamadas a herramientas, prompts 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 con nombre. Claude Code dispara un evento en cada punto en el que está a punto de actuar, por ejemplo, cuando ejecuta una herramienta, envía un prompt, envía una solicitud al modelo, o inicia o finaliza 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).

Crea tu primer mod antes de empezar aquí. Para ver cada evento y sus campos exactos, consulta la referencia o lee los tipos para tu compilación.

Cómo un hook gestiona 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 por sí 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 con el resultado. Lo que tu hook haga con next decide cuál de las tres cosas 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) => {
  // Runs before the tool does
  $.ui.log('Claude is about to use ' + e.tool)
  // Pass the event on unchanged
  return next(e)
})

Antes de que se ejecute cada herramienta, aparece en la transcripción una línea atenuada como ● my-mod: Claude is about to use Bash, 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, usa 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) => {
  // Let the tool run, and wait for its result
  const result = await next(e)
  // Runs after the tool does
  $.ui.log(e.tool + ' finished')
  // Give the result back unchanged
  return result
})

Ahora la línea aparece después de que termina cada herramienta. Claude lee el mismo resultado en ambos casos, porque el hook devuelve aquello con lo que se resolvió next(e).

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 profundidad, y asignar un valor a un campo lanza una excepción. Este hook recorta cada prompt antes de enviarlo:

on('prompt.submit', async ($, e, next) => {
  // Pass on a copy of the event with its text changed
  return next({ ...e, text: e.text.trim() })
})

Los manejadores posteriores y Claude Code reciben el prompt recortado y nunca ven el original. También puedes cambiar el resultado: usa await next(e) y luego devuelve una copia del resultado con un campo reemplazado.

Responder un evento

Para gestionar un evento tú mismo, devuelve un resultado sin llamar a next. Eso interrumpe la cadena, por lo que los mods posteriores y el comportamiento propio de Claude Code no se ejecutan. Este hook rechaza todos los comandos de Bash:

on('tool.call', { tool: 'Bash' }, async () => {
  // No call to next, so the command never runs
  return { deny: 'Bash is turned off in this project. Use the file tools.' }
})

Cuando Claude intenta un comando de Bash, el comando no se ejecuta, y Claude lee el texto de deny como el resultado de la herramienta. Cada evento tiene su propia forma de resultado, que se indica en la referencia de eventos.

Filtrar qué eventos gestiona un hook

Para ejecutar un hook solo para algunos eventos, pasa un filtro como segundo argumento de 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 todos los campos coinciden. Un campo puede ser un valor, un array de valores permitidos o una expresión regular.

Cada línea de este ejemplo registra la misma función, hook, para un conjunto más reducido de llamadas a herramientas:

// A string matches one value: Bash calls only
on('tool.call', { tool: 'Bash' }, hook)
// An array matches any value in it: Edit calls and Write calls
on('tool.call', { tool: ['Edit', 'Write'] }, hook)
// A regular expression matches by pattern: every tool of one MCP server
on('tool.call', { tool: /^mcp__github__/ }, hook)

hook se ejecuta una vez para una llamada a Bash, Edit o Write, y una vez para una llamada a una herramienta cuyo nombre empieza por 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 todos los eventos de hooks de configuración. '*' coincide con todos los eventos excepto los eventos de telemetría, que requieren su propio nombre y un filtro { to: 'collector' }.

Registra cada evento una sola vez por matcher. Si llamas a on dos veces para session.start sin matcher, el módulo no se carga y muestra on("session.start") is registered twice without a matcher. Pon todo lo que tu mod hace al inicio de la sesión en un solo hook.

Engancha 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 un hook puede devolver, consulta la referencia de eventos.

Proteger o cambiar 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 verificación de permisos y luego la herramienta.

Este hook rechaza un comando de Bash que hace un force push 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 falló. La vista que Claude tiene de la llamada no cambia, porque el hook devuelve el resultado que recibió.

Para cambiar una llamada, pasa 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.

Retener 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 verificació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.ask se resuelve con el texto escrito. El hook lo compara con Run it, así que cualquier otro texto rechaza el comando.
  • Nadie responde: $.ui.ask se rechaza cuando el usuario descarta la pregunta o elige Chat about this, y en una ejecución de claude -p, así que el bloque catch deja la respuesta en Refuse

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 dedicado a esperar una promesa propia sí cuenta. Claude Code omite un hook que agota el tiempo, así que el comando retenido se ejecutaría.

Aprobar o rechazar 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 otro hook registró.

Este hook rechaza git push mientras la rama actual sea 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. Extender permisos con hooks indica qué decisiones prevalecen sobre un mod.

Reescribir o agregar contenido a un prompt

Un hook prompt.submit ve cada prompt antes de que comience 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.

Seguir 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 Comienza 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 está definido 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ó, en cuyo caso 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 definido. 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 por streaming. yield* next(e) reenvía la respuesta a medida que se transmite y se evalúa como el resultado terminado. Este hook registra cuánto 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 igual que 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 solo quieras la conversación principal.

Manejar 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 por 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 con otros mods

Varios mods pueden manejar el mismo evento, y cualquiera de ellos puede fallar. Si tu mod bloquea llamadas a herramientas, revisa su posición en la cadena y qué sucede cuando su hook falla.

El orden en que se ejecutan los mods

Los hooks de un mismo evento forman una única cadena de middleware. El next de cada mod llama al hook del mod siguiente, 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 demás y el resultado después de ellos, y decide si los demás se ejecutan o no. Un mod posterior no puede impedir que uno anterior vea un evento.

Claude Code ordena la cadena según el origen de cada mod:

  1. La protección integrada sec-default@builtin, un mod integrado en Claude Code que /plugin muestra como cc-plugin-sec-default, donde se carga, los mods que tu organización incluye en prependPlugins y, luego, cualquier otro mod que se considere de tu organización y que no esté en appendPlugins
  2. Los mods que instalas
  3. Los mods que tu organización incluye en appendPlugins
  4. Otros mods integrados en Claude Code

Entre los mods que instalas, un mod se ejecuta antes que los mods que incluye en dependencies en su manifiesto. Dentro de un mismo módulo, los hooks se ejecutan en el orden en que register llamó a on.

Dónde se ubican los hooks de configuración en el orden

Los hooks PreToolUse configurados en archivos de configuración también se ejecutan durante una llamada a herramienta, en puntos fijos de la cadena de mods:

  • Hooks PreToolUse de la configuración administrada: se ejecutan antes del hook tool.call del primer mod, y un bloqueo de uno de ellos es definitivo, así que ningún mod ve la llamada.
  • Hooks PreToolUse de cualquier otro archivo de configuración y del hooks/hooks.json de los plugins: se ejecutan después de que el último mod llama a next, como parte del comportamiento propio de Claude Code. Un mod que responde a tool.call sin llamar a next impide que se ejecuten, y un mod que llama a next ve su decisión en el resultado que devuelve.

tool.check se activa después de que esos hooks y las reglas de permisos hayan decidido, así 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 interrumpe la sesión, y puedes decidir qué sucede en su lugar. Cuando un hook sin un controlador .catch lanza una excepción, agota el tiempo de espera o devuelve un resultado con una forma incorrecta, lo que sucede a continuación depende de si había llamado a next:

  • Falló antes de llamar a next: Claude Code lo omite, y el siguiente controlador se ejecuta en su lugar
  • Falló después de que next se resolvió: ese resultado se mantiene, y nada se ejecuta por segunda vez

Una línea indica el mod, el evento y el motivo, por ejemplo my-mod: tool.call hook skipped: threw Error: boom. Dónde la lees depende de la sesión, como se indica en Averiguar por qué un mod no hace nada. Un hook ui.render cuyo dibujo no se valida se informa de otra manera, como se describe en Construir un árbol a partir de elementos.

Para que un hook que bloquea llamadas falle de forma cerrada, agrega un controlador de errores .catch que responda en su lugar. Aquí, guard es tu función de hook:

// on returns a registration, and .catch attaches a handler to that one hook
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
  // next.error.kind is 'throw' or 'timeout', which says how guard failed
  return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})

Mientras guard funciona, el controlador nunca se ejecuta. Cuando guard lanza una excepción o agota el tiempo de espera en una llamada a Bash, Claude Code llama al controlador con el mismo evento. El controlador devuelve { deny }, así que el comando no se ejecuta, y Claude lee el texto con throw o timeout al final. Sin el controlador, Claude Code omitiría guard y ejecutaría el comando. El controlador tiene su propio límite de tiempo, más corto.

Próximos pasos