Usar la API de mods
Llamar a la API de mods desde un mod de Claude Code para agregar comandos y herramientas, llamar a un modelo, ejecutar trabajo en un temporizador, enviar mensajes a otras sesiones y acceder a archivos y la red.
La API de mods es el conjunto de métodos que un mod llama para actuar: agregar comandos y herramientas, llamar a un modelo, ejecutar trabajo entre eventos y acceder al sistema de archivos, procesos y la red. Cada hook la recibe como su primer argumento, $, con los métodos agrupados en espacios de nombres como $.ui y $.fs. Los eventos deciden cuándo se ejecuta un hook, y la API de mods es lo que el hook llama una vez que lo hace.
Construya su primer mod antes de comenzar aquí. Para cada método, consulte métodos de la API de mods o lea los tipos para su compilación.
Agregar un comando o una herramienta
Un mod puede agregar un comando para que el usuario ejecute y una herramienta para que Claude llame. Registre ambos en un hook session.start. Claude Code espera ese hook antes del primer prompt, por lo que lo que registre está disponible desde el primer turno.
Agregar un comando
Un comando es para el usuario. Regístrelo y luego maneje command.run para su nombre. Este ejemplo agrega un comando /standup que toma un número opcional de días:
on('session.start', async ($, e, next) => {
// Add /standup to the command list, with the description the user sees there
await $.command.register({ name: 'standup', description: 'Summarize what changed today', argumentHint: '[days]' })
return next(e)
})
// The matcher limits the hook to /standup, so other commands don't reach it
on('command.run', { command: 'standup' }, async ($, e) => {
// e.args is the text typed after the command name, or an empty string
return { text: 'Summary for the last ' + (e.args || '1') + ' day(s): ...' }
})
Después de que la sesión comienza, /standup aparece con su descripción en la lista que ve cuando escribe /. El argumentHint se muestra en el prompt después de que escribe el comando y un espacio, como en /standup [days]. Cuando ejecuta /standup 3, el segundo hook devuelve Summary for the last 3 day(s): ..., y la transcripción muestra ese texto después del nombre del plugin. El hook nunca llama a next, porque el comando no tiene comportamiento que no sea el suyo.
El text que devuelve se imprime en la transcripción y Claude lo lee. Para no imprimir nada, como un comando que solo abre un pane, devuelva {}. Para permitir que el comando se ejecute mientras Claude está trabajando, agregue immediate: true al registro.
Elija un nombre que ningún comando integrado use. Escriba / en una sesión para verlos. $.command.register lanza una excepción para un nombre tomado, con un mensaje como "/focus" refused: it is the built-in /focus". Un hook que lanza una excepción se omite, por lo que el resto de su hook session.start tampoco se ejecuta. Registre comandos al final en ese hook, o envuelva la llamada en try y catch.
Agregar una herramienta
Una herramienta es para Claude. Regístrela con un nombre, una descripción que Claude lee y un JSON Schema para su entrada. Claude la ve bajo un nombre más largo hecho de mcp__, el nombre de su plugin, dos guiones bajos y el nombre que registró. Maneja sus llamadas en un hook tool.call filtrado a ese nombre completo. Este ejemplo, de un plugin llamado my-mod, registra ticket, por lo que el nombre completo es mcp__my-mod__ticket. Le da a Claude una herramienta que busca un ticket en un rastreador de problemas:
on('session.start', async ($, e, next) => {
await $.tool.register({
name: 'ticket',
// Claude decides when to call the tool from this description
description: 'Look up a ticket by its id and return its title and status',
// The arguments Claude has to send: one required string named id
inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
})
return next(e)
})
// The full tool name is mcp__, the plugin's name, and the registered name
on('tool.call', { tool: 'mcp__my-mod__ticket' }, async ($, e) => {
// The tool's arguments are fields of e, so the id is e.id
const response = await $.http.fetch('https://tickets.example.com/api/' + encodeURIComponent(e.id))
// Return a result either way, so Claude learns when the lookup failed
return { result: response.ok ? response.text : 'Lookup failed with status ' + response.status }
})
Cuando pregunta sobre un ticket, Claude puede llamar a mcp__my-mod__ticket con su id. El segundo hook obtiene el ticket y devuelve el cuerpo de la respuesta, que Claude lee como el resultado de la herramienta. Cuando el servidor responde con un estado de error, Claude lee Lookup failed with status y el número.
Llamar a un modelo
Un mod puede hacer una pregunta a un modelo por su cuenta, fuera de la conversación, para un trabajo pequeño como ordenar o resumir un fragmento de texto. $.model.complete envía un prompt a un modelo con las credenciales de su sesión y se resuelve en la respuesta. No tiene historial de conversación.
Este hook responde a un comando /triage, registrado como un comando, pidiendo a un modelo pequeño que etiquete el texto escrito después de él:
on('command.run', { command: 'triage' }, async ($, e) => {
const r = await $.model.complete({
model: 'haiku',
// The system prompt sets the job, and the prompt carries the text to label
system: 'Reply with one word: bug, feature, or question.',
prompt: e.args,
// One word needs few tokens, and the call gives up after 15 seconds
maxTokens: 20,
timeoutMs: 15000,
})
// r.text exists only when the model answered, so check r.isAnswered first
const label = r.isAnswered ? r.text.trim() : 'unknown'
return { text: 'Label: ' + label }
})
Cuando ejecuta /triage the export button does nothing, el mod envía ese texto al modelo e imprime su respuesta, como Label: bug. La conversación de Claude no es parte de la solicitud. Cuando el modelo no responde, la etiqueta es unknown.
Una falla de la API de Claude no rechaza la llamada, por lo que verifique r.isAnswered y lea r.reason cuando es false. La llamada solo rechaza una solicitud que Claude Code no enviará, como un modelo que su organización bloquea. Los tipos para su compilación enumeran las otras opciones, como effort, y los límites dan el valor predeterminado de maxTokens.
$.model.fork({ prompt }) hace una pregunta sobre la conversación actual en su lugar, con el mismo modelo y prompt del sistema, por lo que la API de Claude sirve la mayoría de ella desde el caché de prompts.
Estas llamadas usan el plan o la clave API del usuario.
Ejecutar trabajo en segundo plano
El trabajo que sobrevive a un evento, como verificar algo una vez por minuto, se ejecuta en un temporizador que inicia desde session.start. Un hook en sí se ejecuta para un evento y tiene un límite de tiempo de 10 segundos de su propio tiempo de ejecución. El tiempo dedicado a esperar en next o en una llamada de la API de mods no cuenta, excepto un $.clock.sleep. $.clock.every y $.clock.after toman el lugar de setInterval y setTimeout, con el retraso en milisegundos primero: $.clock.after(5000, fn) llama a fn una vez, cinco segundos a partir de ahora. Cada uno devuelve un temporizador con un método cancel(), y await $.clock.now() da la hora en milisegundos.
Este hook busca las comprobaciones de una solicitud de extracción una vez por minuto y muestra el resultado bajo el prompt. summarize es una función propia que convierte la salida JSON del comando en pocas palabras:
on('session.start', async ($, e, next) => {
// Call the function every 60,000 milliseconds, starting one minute from now
$.clock.every(60_000, async () => {
const status = await $.process.run(['gh', 'pr', 'checks', '--json', 'state'])
// Replace the line under the prompt with the latest summary
$.ui.status('checks: ' + summarize(status.stdout))
})
// Return without waiting for the timer, so the session starts right away
return next(e)
})
La sesión comienza como de costumbre. Un minuto después, aparece una línea bajo el prompt con un ⚠, el nombre del mod y luego checks: y su resumen. Se reemplaza una vez por minuto después de eso. La devolución de llamada del temporizador se ejecuta fuera de cualquier evento, por lo que sigue ejecutándose entre turnos y no inicia uno. Si la devolución de llamada lanza una excepción, el error va al registro de depuración y el temporizador se ejecuta nuevamente en el siguiente intervalo.
Mostrar algo sin iniciar un turno
Un trabajo en segundo plano puede mostrar al usuario algo sin iniciar un turno. Cada una de estas llamadas pone texto en un lugar diferente:
| Llamada | Lo que el usuario ve |
|---|---|
$.ui.status(text) |
Una línea bajo el prompt que permanece hasta que la cambie. Comienza con ⚠ y el nombre del mod, como en ⚠ my-mod: checks: 3 passing. |
$.ui.toast(text) |
Una pequeña caja en la esquina superior derecha, con el nombre del mod encima del texto, que desaparece después de unos segundos |
$.ui.log(text) |
Una línea tenue en la transcripción que Claude no lee. Comienza con ● y el nombre del mod, como en ● my-mod: build finished. |
Iniciar un turno desde un trabajo en segundo plano
Cuando un trabajo en segundo plano encuentra algo que necesita la atención de Claude, puede iniciar un turno enviando un prompt con $.prompt.submit({ text }). Claude lee el texto después de una oración que nombra su mod como el remitente. Para enviarlo como las propias palabras del usuario, sin esa oración, agregue asUser: true. La llamada espera hasta que la sesión esté inactiva y luego inicia un nuevo turno. Se resuelve cuando ese turno comienza, por lo que no lo await en un controlador que se ejecuta mientras Claude está trabajando.
Detener el trabajo en segundo plano
El trabajo en segundo plano se detiene de dos formas. Los temporizadores se detienen cuando el módulo se recarga. Para trabajo de larga duración dentro de un hook, next.signal es un AbortSignal que se cancela cuando se abandona el evento que maneja su hook, por ejemplo cuando el usuario interrumpe, por lo que páselo a cualquier cosa de larga duración.
Enviar y recibir mensajes entre sesiones
Un mod puede enviar un mensaje de texto sin formato a otra de sus sesiones o a uno de los subagentes de esta sesión, y observar los mensajes que llegan y se van. $.session.send({ to, text }) envía uno, la misma entrega que hace la herramienta SendMessage. to es { sessionId } para una sesión, { agentId } para un subagente de $.agent.list(), o la dirección de cadena de la que provino un mensaje recibido. La llamada se resuelve una vez que el mensaje se pone en cola, con { isDelivered: true }. Cuando nada fue entregado se resuelve con { isDelivered: false, reason }, y reason dice por qué.
Este hook responde a un comando /ping, registrado como un comando, pidiendo a la sesión cuyo id escribe después de él un estado:
on('command.run', { command: 'ping' }, async ($, e) => {
// e.args is the session id typed after /ping
const sent = await $.session.send({ to: { sessionId: e.args }, text: 'Status? One line.' })
// The call resolves either way, so check isDelivered to learn what happened
if (!sent.isDelivered) $.ui.toast('Not delivered: ' + sent.reason)
// An empty result prints nothing in this session's transcript
return {}
})
Cuando el mensaje se pone en cola, nada aparece en su sesión, y Claude de la otra sesión lee Status? One line. Cuando nada fue entregado, una pequeña caja en la esquina superior derecha da la razón y desaparece después de unos segundos.
Dos eventos permiten que un mod observe los mensajes. Devuelva next(e) de ambos para pasar cada mensaje sin cambios:
| Evento | Se dispara cuando | Campos útiles |
|---|---|---|
session.receive |
Un mensaje llega para esta sesión, antes de que Claude lo lea | e.text, y e.origin.kind, como peer o peer-send-message para otra sesión o agente, task-notification, o scheduled-trigger. Devuelva { consumed: reason } para evitar que llegue a Claude. |
session.send |
Un mensaje está a punto de salir, desde la herramienta SendMessage o un mod | e.to, e.text, y e.origin.kind, que es model o plugin |
Una sesión configurada para rechazar mensajes entrantes rechaza un mensaje antes de que se dispare session.receive, por lo que un hook nunca lo ve. Un mensaje que se retiene para su aprobación llega al hook primero, por lo que un mod puede leer un mensaje que aún no ha aprobado. El next(e) del hook rechaza cuando el mensaje no se entrega.
El nombre del remitente en un mensaje recibido es lo que escribió el remitente, por lo que no base una decisión en él.
Acceder a archivos, procesos y la red
Un mod accede al sistema de archivos, procesos y la red a través de la API de mods, con los mismos permisos que el usuario que ejecuta Claude Code. El módulo de hooks en sí no tiene APIs de Node.js, sin globales de temporizador como setTimeout, y sin acceso a la red o archivos propios. Las APIs estándar de JavaScript y web como URL, TextEncoder, AbortController y crypto.subtle están disponibles. Cada espacio de nombres a continuación cubre un tipo de acceso:
| Espacio de nombres | Lo que hace |
|---|---|
$.fs |
read(path), write(path, text), exists(path), stat(path) y list(path) funcionan en archivos y directorios |
$.process |
run(['git', 'status']) inicia un comando y se resuelve cuando sale. spawn transmite la salida de un comando de larga duración. |
$.http |
fetch(url, init) sobre http o https. Se resuelve a { status, ok, headers, text } una vez que se lee el cuerpo. |
$.store |
Un almacén de clave-valor JSON propio de su plugin, mantenido entre sesiones |
$.env |
get y set variables de entorno. Escriba el nombre como una cadena literal. |
$.settings |
read lo que los archivos de configuración y la política administrada contienen |
$.session |
messages() devuelve la transcripción como una lista de { role, text, toolUses }. También el directorio de trabajo, modelo y más. usage() devuelve el uso de la ventana de contexto y los límites del plan. |
$.mcp |
call una herramienta en un servidor MCP conectado |
Los archivos y procesos tienen algunas reglas propias:
- Rutas: una ruta relativa está bajo el directorio de trabajo de la sesión
$.fs.list: devuelve las entradas de un directorio como{ name, kind, size, isLink }y no desciende a subdirectorios$.process.run: toma una lista de argumentos y no usa shell. Se resuelve a{ exitCode, stdout, stderr }sea cual sea el código de salida. Rechaza si el programa no puede iniciarse o sigue ejecutándose en el tiempo de espera, que es de 30 segundos por defecto, por lo que envuélvalo entryycatch.
Cada una de estas llamadas es en sí misma un evento, nombrado para su espacio de nombres y método sin el $., como fs.read para $.fs.read. Un mod anterior en la cadena puede observar, reescribir o rechazar su llamada, que es cómo una organización restringe lo que los mods alcanzan.
Próximos pasos
- Reaccionar a eventos: hook de llamadas de herramientas, prompts y turnos
- Dibujar en la interfaz: mostrar lo que su mod recopila en un pane o encima del prompt
- Probar un mod: stub cualquiera de estas llamadas en una prueba
- Referencia de mods: cada evento, cada método de la API de mods y los límites