SpyBara
Go Premium

plugins/mods/events.md 2026-09-30 23:00 UTC to 2026-10-01 21:59 UTC

This page contains 336 additions and 0 deletions.

2026
Thu 1 23:02

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

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:

  1. El guardia incorporado sec-default@builtin, un mod incorporado en Claude Code que /plugin enumera como cc-plugin-sec-default, donde se carga, mods que tu organización enumera en prependPlugins, y luego cualquier otro mod que cuente como de tu organización y no esté en appendPlugins
  2. Mods que instalas
  3. Mods que tu organización enumera en appendPlugins
  4. 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 PreToolUse de configuración administrada: se ejecutan antes que el hook tool.call del primer mod, y un bloqueo de uno de ellos es final, por lo que ningún mod ve la llamada.
  • Hooks PreToolUse de cada otro archivo de configuración y de hooks/hooks.json de 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 tool.call sin llamar a next los mantiene de ejecutarse, y un mod que llama a next ve 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 next se 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