SpyBara
Go Premium

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

This page contains 56 additions and 56 deletions.

2026
Thu 1 23:59 Fri 2 22:00

Usa la API de mods

Llama a la API de mods desde un mod de Claude Code para agregar comandos y herramientas, llamar a un modelo, ejecutar trabajo con un temporizador, enviar mensajes a otras sesiones y acceder a archivos y a 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, a los procesos y a 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 se ejecuta.

Crea tu primer mod antes de empezar aquí. Para ver todos los métodos, consulta métodos de la API de mods o lee los tipos para tu compilación.

Agregar un comando o una herramienta

Un mod puede agregar un comando para que el usuario lo ejecute y una herramienta para que Claude la llame. Registra ambos en un hook session.start. Claude Code espera a ese hook antes del primer prompt, así que lo que registres está disponible desde el primer turno.

Agregar un comando

Un comando es para el usuario. Regístralo y luego maneja command.run para su nombre. Este ejemplo agrega un comando /standup que acepta 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 inicia, /standup aparece con su descripción en la lista que ves cuando escribes /. El argumentHint se muestra en el prompt después de que escribes el comando y un espacio, como en /standup [days]. Cuando ejecutas /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 otro comportamiento además del tuyo.

El text que devuelves se imprime en la transcripción y Claude lo lee. Para no imprimir nada, como hace un comando que solo abre un panel, devuelve {}. Para permitir que el comando se ejecute mientras Claude está trabajando, agrega immediate: true al registro.

Elige un nombre que no use ningún comando integrado. Escribe / en una sesión para verlos. $.command.register lanza un error para un nombre ya ocupado, con un mensaje como "/focus" refused: it is the built-in /focus. Un hook que lanza un error se omite, así que el resto de tu hook session.start tampoco se ejecuta. Registra los comandos al final de ese hook, o envuelve la llamada en try y catch.

Agregar una herramienta

Una herramienta es para Claude. Regístrala con un nombre, una descripción que Claude lee y un JSON Schema para su entrada. Claude la ve con un nombre más largo formado por mcp__, el nombre de tu plugin, dos guiones bajos y el nombre que registraste. Manejas 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 gestor de incidencias:

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 preguntas por 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 código de estado de error, Claude lee Lookup failed with status y el número.

Llamar a un modelo

Un mod puede hacerle a un modelo una pregunta propia, fuera de la conversación, para una tarea pequeña como clasificar o resumir un fragmento de texto. $.model.complete envía un prompt a un modelo con las credenciales de tu sesión y se resuelve con la respuesta. No tiene historial de conversación.

Este hook responde a un comando /triage, registrado como comando, pidiéndole 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 ejecutas /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 forma parte de la solicitud. Cuando el modelo no responde, la etiqueta es unknown.

Un fallo de la API de Claude no rechaza la llamada, así que comprueba r.isAnswered y lee r.reason cuando sea false. La llamada se rechaza ante una solicitud que Claude Code no enviará, como un modelo que tu organización bloquea. Los tipos de tu compilación enumeran las demás opciones, como effort, y los límites indican el valor predeterminado de maxTokens.

$.model.fork({ prompt }), en cambio, hace una pregunta sobre la conversación actual, con el mismo modelo y el mismo prompt del sistema, de modo que la API de Claude sirve la mayor parte desde la caché de prompts.

Estas llamadas usan el plan o la clave de API del usuario.

Ejecutar trabajo en segundo plano

El trabajo que dura más que un evento, como revisar algo una vez por minuto, se ejecuta en un temporizador que inicias desde session.start. Un hook en sí se ejecuta para un evento y tiene un límite de tiempo sobre su propio tiempo de ejecución. El tiempo dedicado a esperar a next o a una llamada a la API de mods no cuenta, excepto un $.clock.sleep. $.clock.every y $.clock.after reemplazan a setInterval y setTimeout, con el retraso en milisegundos primero: $.clock.after(5000, fn) llama a fn una vez, dentro de cinco segundos. Cada uno devuelve un temporizador con un método cancel(), y await $.clock.now() da la hora en milisegundos.

Este hook consulta las comprobaciones de un pull request una vez por minuto y muestra el resultado debajo del prompt. summarize es una función propia que convierte la salida JSON del comando en unas 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 se inicia como de costumbre. Un minuto después, aparece una línea debajo del prompt con un ⚠, el nombre del mod y luego checks: y tu resumen. Se reemplaza una vez por minuto a partir de entonces. La función de callback del temporizador se ejecuta fuera de cualquier evento, por lo que sigue ejecutándose entre turnos y no inicia ninguno. Si la función de callback lanza una excepción, el error va al registro de depuración y el temporizador se ejecuta de nuevo en el siguiente intervalo.

Mostrar algo sin iniciar un turno

Un trabajo en segundo plano puede mostrarle algo al usuario sin iniciar un turno. Cada una de estas llamadas coloca texto en un lugar diferente:

Llamada Lo que ve el usuario
$.ui.status(text) Una línea debajo del prompt que permanece hasta que la cambies. Comienza con ⚠ y el nombre del mod, como en ⚠ my-mod: checks: 3 passing.
$.ui.toast(text) Una notificación emergente en la parte superior derecha, con el nombre del mod encima del texto, que desaparece después de unos segundos
$.ui.log(text) Una línea atenuada 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 a tu mod como remitente. Para enviarlo como las propias palabras del usuario, sin esa oración, agrega asUser: true. La llamada espera hasta que la sesión esté inactiva y luego inicia un nuevo turno. Se resuelve cuando ese turno comienza, así que no uses await con ella en un handler que se ejecute mientras Claude está trabajando.

Detener el trabajo en segundo plano

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 aborta cuando se abandona el evento que tu hook está manejando, por ejemplo cuando el usuario interrumpe, así que pásalo 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 tus sesiones o a uno de los subagentes de esta sesión, y observar los mensajes que llegan y salen. $.session.send({ to, text }) envía uno, con la misma entrega que realiza la herramienta SendMessage. to es { sessionId } para una sesión, { agentId } para un subagente de $.agent.list(), o la dirección en forma de cadena de la que provino un mensaje recibido. La llamada se resuelve en cuanto el mensaje queda en cola, con { isDelivered: true }. Cuando no se entregó nada, se resuelve con { isDelivered: false, reason }, y reason indica el motivo.

Este hook responde a un comando /ping, registrado como comando, pidiéndole un estado a la sesión cuyo id escribes después:

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 queda en cola, no aparece nada en tu sesión, y el Claude de la otra sesión lee Status? One line. Cuando no se entregó nada, una notificación emergente indica el motivo.

session.receive y session.send permiten que un mod observe los mensajes. Devuelve next(e) desde ambos para dejar pasar cada mensaje sin cambios:

Evento Se activa cuando Campos útiles
session.receive Llega un mensaje 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. Devuelve { 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 active session.receive, por lo que un hook nunca lo ve. Un mensaje que queda retenido a la espera de tu aprobación llega primero al hook, así que un mod puede leer un mensaje que aún no has aprobado. El next(e) del hook se rechaza cuando el mensaje no se entrega.

El nombre del remitente en un mensaje recibido es lo que el remitente haya escrito, así que no bases ninguna decisión en él.

Accede a archivos, procesos y la red

Un mod accede al sistema de archivos, a los procesos y a 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, ni globales de temporizador como setTimeout, ni acceso propio a la red o a los archivos. Están disponibles las API estándar de JavaScript y de la web, como URL, TextEncoder, AbortController y crypto.subtle. Cada espacio de nombres a continuación cubre un tipo de acceso:

Espacio de nombres Qué hace
$.fs read(path), write(path, text), exists(path), stat(path) y list(path) trabajan con archivos y directorios
$.process run(['git', 'status']) inicia un comando y se resuelve cuando este termina. spawn transmite la salida de un comando de larga duración.
$.http fetch(url, init) sobre http o https. Se resuelve en { status, ok, headers, text } una vez que se lee el cuerpo.
$.store Un almacén clave-valor JSON propio de tu plugin, que se conserva entre sesiones
$.env get y set de variables de entorno. Escribe el nombre como un literal de cadena.
$.settings read lo que contienen los archivos de configuración y la política administrada
$.session messages() devuelve la transcripción como una lista de { role, text, toolUses }. También el directorio de trabajo, el modelo y más. usage() devuelve el uso de la ventana de contexto y los límites del plan.
$.mcp call a una herramienta en un servidor MCP conectado

Los archivos y los procesos tienen algunas reglas propias:

  • Rutas: una ruta relativa se resuelve con respecto al directorio de trabajo de la sesión
  • $.fs.list: devuelve las entradas de un directorio como { name, kind, size, isLink } y no es recursivo
  • $.process.run: recibe una lista de argumentos y no usa ningún shell. Se resuelve en { exitCode, stdout, stderr } sea cual sea el código de salida. Se rechaza si el programa no puede iniciarse o si sigue en ejecución al cumplirse el tiempo de espera, que es de 30 segundos de forma predeterminada, así que envuélvelo en try y catch.

Cada una de estas llamadas es en sí misma un evento, nombrado según su espacio de nombres y su método sin el $., como fs.read para $.fs.read. Un mod anterior en la cadena puede observar, reescribir o rechazar tu llamada, que es la forma en que una organización restringe aquello a lo que acceden los mods.

Próximos pasos