SpyBara
Go Premium

plugins/mods/api.md 2026-11-01 23:57 UTC to 2026-11-02 23:57 UTC

This page contains 1 addition and 1 deletion.

2026
Mon 2 23:57

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.

[!TIP]

Cuando MCP Tool Search difiere una herramienta registrada, Claude ve su nombre pero no su descripción hasta que la busca. Si Claude debe considerar la herramienta en cada turno, agrega isDeferred: false al registro para cargar la herramienta completa desde el inicio. El campo requiere Claude Code v2.1.293 o posterior, y las versiones anteriores lo ignoran.

Llamar a un modelo

Un mod puede enviar sus propias solicitudes a un modelo para una tarea pequeña como clasificar o resumir texto. $.model.complete envía tu prompt por sí solo, y $.model.fork({ prompt }) envía la conversación actual con tu prompt al final.

Esta tabla compara lo que contiene cada solicitud:

En la solicitud $.model.complete $.model.fork
Modelo El model que pasas El modelo de la sesión
Prompt del sistema Un breve bloque de atribución y luego tu system si pasas uno El prompt del sistema de la sesión
Mensajes Un mensaje de usuario, tu prompt La conversación hasta el momento y luego tu prompt como mensaje de usuario
CLAUDE.md y otro contexto del proyecto No se incluye Se incluye, como en la última solicitud de la conversación
Herramientas Ninguna Las herramientas de Claude, que el modelo no puede llamar

Un fork repite la última solicitud de la conversación, por lo que la API de Claude sirve la mayor parte desde la caché de prompts mientras la conversación siga en caché.

Ambas llamadas usan las credenciales de la sesión, por lo que se facturan al plan, la clave de API o el proveedor de nube del usuario. Los tipos de tu compilación documentan todos los métodos de $.model.

Enviar un prompt

Pasa model y prompt a $.model.complete. prompt se convierte en el mensaje de usuario. Para darle instrucciones al modelo, como un rol o un formato de salida, pasa también system, que se convierte en el prompt del sistema.

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. 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.

Usar la caché de prompts

$.model.complete admite la caché de prompts de la API de Claude. La API almacena en caché el inicio de una solicitud, llamado prefijo, hasta un punto de interrupción de caché que tú defines. Cuando todas las llamadas empiezan con el mismo contenido estático extenso, como instrucciones o material de referencia, define un punto de interrupción al final de ese contenido. Así, las llamadas posteriores lo leen desde la caché en lugar de pagar el precio completo de entrada por él.

Para definir un punto de interrupción, pasa prompt como un arreglo de bloques { text } en lugar de una cadena, y agrega cache: true al último bloque del contenido estático. Claude Code envía ese bloque con el campo cache_control de la API. system acepta la misma forma de arreglo. Para decidir entre ambos, consulta Elegir entre prompt y system.

[!NOTE]

Los arreglos de bloques requieren Claude Code v2.1.292 o posterior. Las versiones anteriores rechazan un arreglo en prompt con un error que termina en takes { model, prompt } (host check), y omiten de la solicitud un arreglo en system.

Esta versión del hook /triage envía un conjunto extenso de reglas de etiquetado antes del texto a etiquetar, con un punto de interrupción después de las reglas. RULES es una cadena propia:

on('command.run', { command: 'triage' }, async ($, e) => {
  const r = await $.model.complete({
    model: 'haiku',
    prompt: [
      // Identical on every call, so it forms the cached prefix
      { text: RULES, cache: true },
      // Changes on every call, so it goes after the breakpoint
      { text: e.args },
    ],
  })
  return { text: 'Label: ' + (r.isAnswered ? r.text.trim() : 'unknown') }
})

El TTL y la cantidad de puntos de interrupción tienen estos límites:

  • TTL: una entrada de caché dura cinco minutos después de su último uso. El TTL proviene de la configuración de Claude Code del usuario, no de la llamada. Para una hora, establece subagentPromptCacheTtl en 1h.
  • Puntos de interrupción por solicitud: la API acepta hasta cuatro, y uno más vuelve como un api-error en r.reason

Elegir entre prompt y system

Coloca el contenido estático que comparten tus llamadas al inicio de prompt, a menos que sepas que tus solicitudes van directamente a la API de Claude:

  • Directamente a la API de Claude, con una clave de API o una suscripción a Claude: cualquiera de los dos campos funciona
  • A través de Amazon Bedrock, Claude Platform on AWS, Agent Platform de Google Cloud, Microsoft Foundry o un gateway de LLM: usa prompt. Claude Code comienza el prompt del sistema con un bloque de atribución cuya huella digital proviene del inicio del mensaje de usuario. El endpoint api.anthropic.com elimina ese bloque antes de almacenar en caché. Otros endpoints lo reciben como parte del prompt, por lo que un punto de interrupción en system puede fallar cuando prompt empieza de forma distinta.
  • En un mod que ejecutan otras personas: usa prompt, porque tú no eliges su proveedor

system va antes que prompt en el prefijo, por lo que un punto de interrupción en prompt también cubre system, y una llamada con un system distinto no acierta en la caché.

Comprobar los aciertos de caché

El resultado de $.model.complete tiene un objeto usage con los campos de caché de la API. usage.cache_creation_input_tokens cuenta los tokens que la llamada escribió en la caché, y usage.cache_read_input_tokens cuenta los tokens que leyó de la caché. Espera una escritura en la primera llamada y lecturas en las llamadas posteriores dentro del TTL.

Si todas las llamadas escriben y ninguna lee, el prefijo difiere entre llamadas o las llamadas están más separadas que el TTL. Si el prefijo difiere, consulta Elegir entre prompt y system.

Si ambos campos se quedan en cero en llamadas que el modelo respondió, no se almacenó nada en caché. Comprueba cada una de estas causas:

Qué recibe un hook de model.complete

Si conectas un hook al evento model.complete para inspeccionar o modificar las solicitudes de otros mods, lee el texto desde estos campos:

  • e.prompt: siempre es una cadena. Cuando quien llama pasó un arreglo, es el texto de los bloques concatenado en orden.
  • e.system: una cadena construida de la misma forma, o ausente cuando quien llama no pasó system
  • e.promptBlocks y e.systemBlocks: los arreglos de quien llama, cada uno presente cuando quien llama pasó un arreglo para ese campo

Claude Code envía las cadenas que tu hook pasa a next y usa los arreglos que pasas con ellas para ubicar los puntos de interrupción de caché. Conserva los bloques iniciales que todavía coinciden con el inicio de la cadena, con sus puntos de interrupción, y envía el resto de la cadena sin punto de interrupción. Por ejemplo, next({ ...e, prompt: e.prompt + NOTE }) conserva los puntos de interrupción de quien llama, y un hook que cambia el inicio de prompt los elimina.

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 con el nombre del mod que desaparece después de unos segundos. Es un cuadro en la parte superior derecha en el renderizado de pantalla completa, y una línea a la derecha debajo del prompt en el renderizador clásico.
$.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.
$.ui.notify(text) Una notificación de su terminal, fuera de Claude Code, enviada de la forma que indica su ajuste preferredNotifChannel. El nombre del mod es su título, a menos que pases un { title } no vacío, que se muestra tal como lo indicas.

$.ui.notify es la única de estas llamadas que responde. Se resuelve con { isSent: true, channel } cuando el canal del usuario escribió la notificación en su terminal. channel es el valor del ajuste que la envió, como kitty o terminal_bell. El canal terminal_bell hace sonar la campana y no muestra ni tu texto ni el título.

De lo contrario, la llamada se resuelve con { isSent: false, reason }:

reason Significado
disabled El usuario estableció preferredNotifChannel en notifications_disabled
no-channel El ajuste es auto y su terminal no tiene ninguna notificación que Claude Code pueda enviar
no-surface La sesión no tiene terminal, como en una ejecución con -p, el Agent SDK, la aplicación de escritorio, la extensión de VS Code o una sesión en la nube. También es la respuesta a una llamada hecha antes de que se inicie la sesión, mientras tu mod se está cargando, y a una llamada hecha mientras Claude Code se está cerrando
refused El hook de otro mod sobre ui.notify respondió en lugar de Claude Code sin enviarla

La llamada se rechaza en lugar de resolverse cuando text no es una cadena, cuando el hook de otro mod sobre ui.notify devuelve { deny }, o cuando falla la escritura en la terminal.

Tu mod no puede elegir el canal, y Claude Code no limita la frecuencia con la que un mod envía notificaciones. El ajuste del usuario es el único interruptor.

Cuando la llamada llega a una sesión en ejecución, se ejecutan los hooks de Notification del usuario con el tipo plugin_notification. Se ejecutan incluso cuando la respuesta es disabled, y en una sesión sin terminal, donde la respuesta es no-surface. No se ejecuta ningún hook para una llamada hecha antes de que se inicie la sesión o mientras Claude Code se está cerrando. El hook desde el que tu mod llamó a $.ui.notify queda fuera de esa ejecución, por lo que un hook classic.Notification que envía notificaciones no se vuelve a llamar para su propia notificación.

Una terminal no informa si mostró la notificación, así que isSent: true significa que se envió, no que el usuario la vio.

Este ejemplo envía una notificación cuando el turno de la conversación principal termina sin una interrupción, y recurre a una notificación emergente cuando no salió ninguna notificación con texto. No espera a la notificación antes de pasar el evento, porque los hooks de Notification del usuario se ejecutan primero y pueden tardar un poco:

on('turn.complete', ($, e, next) => {
  const isMainTurn = e.agentId === undefined && !e.isAborted
  const fallBack = () => $.ui.toast('Turn finished')
  if (isMainTurn) {
    $.ui.notify('Turn finished').then(sent => {
      const hasText = sent.isSent && sent.channel !== 'terminal_bell'
      if (!hasText) fallBack()
    }, fallBack)
  }
  return next(e)
})

En la extensión de VS Code, las líneas de estado y las notificaciones emergentes se muestran encima del cuadro del prompt, y las líneas de registro, en la conversación.

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 después de que 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. Pásalo a cualquier cosa de larga duración.

Llamadas y trabajo en segundo plano después de que el usuario interrumpe un turno

El motivo the session was stopped and nobody has sent input since en un resultado o error significa que el usuario detuvo un turno en ejecución, por ejemplo con Esc o Stop. Después de esa interrupción, las llamadas que inician trabajo nuevo en la sesión se rechazan hasta la siguiente entrada del usuario, como un mensaje en el prompt.

Aparte de $.prompt.submit y $.command.run, este manejo de las llamadas y del trabajo en segundo plano después de una interrupción requiere Claude Code v2.1.304 o posterior.

Reintentar una llamada rechazada o descartada

Vuelve a hacer una llamada rechazada o descartada desde un hook de turn.complete. Las llamadas siguen rechazándose hasta la siguiente entrada del usuario, y nada las pone en cola ni las repite por ti.

Cuando el resultado o el error de una llamada contiene una de estas cadenas, guarda sus argumentos para reintentarla más tarde:

  • the session was stopped and nobody has sent input since: la llamada se rechazó
  • was removed from the queue before it ran: el prompt o el comando salió de la cola sin ejecutarse y se descartó, por ejemplo cuando el usuario detuvo el turno

En este ejemplo, llama a submitPrompt desde tu trabajo en segundo plano en lugar de $.prompt.submit. Coloca la variable pendingText y la función submitPrompt encima de register, y el hook de turn.complete dentro de register, porque la validación rechaza $ pasado a una función declarada dentro de register. Cuando el resultado o el error contiene una de esas cadenas, submitPrompt guarda el texto en pendingText para que el hook de turn.complete lo envíe de nuevo:

// The text of the last refused or dropped prompt, or null
let pendingText = null

async function submitPrompt($, text) {
  let reason
  try {
    const result = await $.prompt.submit({ text })
    reason = result?.drop ?? ''
  } catch (error) {
    reason = String(error?.message ?? error)
  }
  if (reason.includes('the session was stopped and nobody has sent input since') ||
      reason.includes('was removed from the queue before it ran')) {
    pendingText = text
  }
  // Any other result or error isn't retried
}

on('turn.complete', async ($, e, next) => {
  if (pendingText !== null) {
    const text = pendingText
    // Empty pendingText before retrying, so that a second run of this hook
    // doesn't submit the same text.
    pendingText = null
    // Not awaited: the hook would stay open until the new turn starts
    submitPrompt($, text)
  }
  return next(e)
})

Antes de la siguiente entrada del usuario, turn.complete puede dispararse para el propio turno interrumpido y para el turno de un subagente, y las llamadas que se hagan en ese momento se rechazan de nuevo. En ese caso, submitPrompt guarda el texto para un turn.complete posterior.

Para una llamada que se rechaza con un error, como $.command.run, captura el error y comprueba su mensaje de la misma forma en que submitPrompt comprueba reason.

Qué devuelven las llamadas rechazadas

En los resultados, <reason> representa el motivo por el que se rechazó la llamada. Después de una interrupción, ese motivo es the session was stopped and nobody has sent input since. Para otros motivos, consulta Llamadas rechazadas en ejecuciones de claude -p y sesiones del Agent SDK.

Algunas llamadas rechazadas Resultado
$.prompt.submit Se resuelve con { drop: '<reason>' } y no pone ningún prompt en cola
$.command.run Se rechaza con un error cuyo mensaje termina con <reason>
$.agent.spawn Se resuelve con { deny: '<reason>' } y no inicia ningún subagente
$.session.send a un subagente que terminó Se resuelve con { isDelivered: false, reason: 'Agent "<id>" was not resumed: <reason>.' }, donde <id> es el id del subagente. Un subagente que sigue en ejecución recibe el mensaje.
$.session.send a un compañero de un equipo de agentes Se resuelve con { isDelivered: false, reason: 'Teammate "<name>" was not messaged: <reason>.' }, donde <name> es el nombre del compañero
$.tool.call de Bash con run_in_background: true, Monitor o CronCreate Se resuelve con { deny: '<tool> was not run: it starts background work, a schedule or a goal, and <reason>' }, donde <tool> es el nombre de la herramienta

Cuando una de estas llamadas a $.session.send o $.tool.call se rechaza después de una interrupción en una sesión interactiva, el usuario puede ver la notificación emergente Stopped:.

Trabajo que tu mod inició antes de la interrupción

Después de que el usuario detiene un turno en ejecución, por ejemplo con Esc, parte del trabajo que tu mod ya había iniciado se descarta y parte espera a la siguiente entrada del usuario.

Trabajo Qué sucede hasta la siguiente entrada del usuario Qué hacer
Un prompt de $.prompt.submit que sigue en la cola Se descarta. La llamada se resuelve con { drop: 'the prompt was removed from the queue before it ran' }. Envíalo de nuevo como se muestra en Reintentar una llamada rechazada o descartada
Un comando de $.command.run que sigue en la cola Se descarta. La llamada se rechaza con un error cuyo mensaje incluye the command was removed from the queue before it ran. Ejecútalo de nuevo como se muestra en Reintentar una llamada rechazada o descartada
Un trabajo único programado con una herramienta como CronCreate que vence antes de la siguiente entrada del usuario No inicia ningún turno en ese momento y se ejecuta después de esa entrada No hace falta nada
Un trabajo recurrente programado con una herramienta como CronCreate que vence antes de la siguiente entrada del usuario No inicia ningún turno en ese momento. El trabajo pierde esa ejecución y conserva su programación. No hace falta nada
Un evento de un monitor que inició tu mod El monitor sigue en ejecución. Un evento que el monitor genera antes de la siguiente entrada del usuario se descarta, excepto el aviso de que el monitor terminó, que llega después de esa entrada. No se puede hacer nada
Un subagente que tu mod generó y luego reanudó con $.session.send y que termina antes de la siguiente entrada del usuario Su aviso de finalización espera y llega a la conversación después de esa entrada No hace falta nada
Un comando de Bash o PowerShell en primer plano de $.tool.call que ya estaba en ejecución cuando el usuario detuvo el turno y que alcanza su tiempo de espera antes de la siguiente entrada del usuario Se detiene al alcanzar su tiempo de espera en lugar de pasar a segundo plano. La llamada se resuelve con isError establecido, y la salida incluye la línea Ended instead of moving to the background: plugins' input is held, by a Stop or by the host's limits. Ejecútalo de nuevo después de la siguiente entrada del usuario, como se muestra en Reintentar una llamada rechazada o descartada

Cuando un trabajo programado vence o un monitor genera un evento antes de la siguiente entrada del usuario, el usuario puede ver la notificación emergente Stopped:.

Llamadas rechazadas en ejecuciones de claude -p y sesiones del Agent SDK

En una ejecución de claude -p o en una sesión del Agent SDK, un resultado como { drop: 'the run reached its --max-turns limit' } significa que la ejecución rechazó la llamada de tu mod.

Un $.prompt.submit rechazado se resuelve con { drop: '<reason>' }. Otras llamadas rechazadas, como $.command.run y $.agent.spawn, dan los resultados que se muestran en Qué devuelven las llamadas rechazadas, con el motivo en lugar de <reason>. Busca tu <reason> a continuación para saber qué hacer:

  • the run reached its --max-turns limit: los turnos de los prompts de tu mod y de los subagentes que genera cuentan para --max-turns o para el límite de turnos del Agent SDK. Para tener más turnos, inicia la ejecución con un límite más alto. Si no puedes, haz que tu mod deje de hacer la llamada rechazada. Si la ejecución recibe otro prompt del usuario o de la aplicación del SDK, el recuento de turnos se reinicia y la ejecución vuelve a aceptar llamadas.
  • the run is past its --max-budget-usd limit: inicia la ejecución con un --max-budget-usd más alto, o aumenta el límite de gasto del Agent SDK. Si no puedes, haz que tu mod deje de hacer la llamada rechazada. El límite se aplica al costo total de la ejecución hasta el momento. Ese total sigue sumándose cuando la ejecución recibe otro prompt, así que un reintento devuelve el mismo motivo.
  • the host has ended its input: por ejemplo, la aplicación del SDK terminó su entrada en streaming, o stdin se cerró en una ejecución iniciada con --input-format stream-json. La ejecución está terminando y no acepta más entrada, así que un reintento devuelve el mismo motivo. Haz que tu mod deje de hacer la llamada rechazada. Si inicias la ejecución tú mismo, la próxima vez mantén esa entrada abierta hasta que tu mod no tenga más llamadas que hacer.
  • the session was stopped and nobody has sent input since: la ejecución se interrumpió, por ejemplo con interrupt() del Agent SDK. Para volver a hacer la llamada después del siguiente mensaje del usuario o de la aplicación del SDK, sigue Reintentar una llamada rechazada o descartada.

Una ejecución de claude -p con tu mod nunca termina

Si tu mod envía un prompt al final de cada turno, cada prompt inicia otro turno. Sin --max-turns ni --max-budget-usd, una ejecución de claude -p que carga el mod continúa hasta que la detengas tú mismo.

Para que la ejecución termine por sí sola, agrega cualquiera de los dos flags al comando en tu shell. Si otras personas inician la ejecución, haz que tu mod deje de enviar prompts una vez que termine su trabajo. Este comando agrega --max-turns:

claude -p "find the TODOs" --max-turns 5

Una vez que la ejecución alcanza ese límite, el siguiente prompt del mod se rechaza en lugar de iniciar un turno, y la ejecución termina.

Si la ejecución sigue superando el límite, ejecuta claude --version en tu shell y actualiza Claude Code si tu versión es anterior a v2.1.304. Los prompts del mod se rechazan al alcanzar el límite a partir de v2.1.299. Si tu mod genera subagentes, sus turnos cuentan para --max-turns a partir de v2.1.304.

Enviar y recibir mensajes entre sesiones

Un mod puede enviar un mensaje de texto sin formato a otra de tus sesiones, a uno de los subagentes de esta sesión o a un compañero de su equipo de agentes. También puede observar los mensajes que llegan y salen.

Para enviar uno, llama a $.session.send({ to, text }), que realiza la misma entrega que la herramienta SendMessage. Define to según quién recibe el mensaje:

  • Otra de tus sesiones: { sessionId }
  • Un subagente o compañero: { agentId }, con un id de $.agent.list()
  • El remitente de un mensaje que recibiste: la dirección en forma de cadena de la que provino ese mensaje

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() se resuelve en toda la configuración, combinada en un solo objeto. Con { source } o { key }, se resuelve en cambio en la configuración de una fuente o el valor de una clave.
$.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, los procesos y las solicitudes tienen algunas reglas propias:

  • Rutas: una ruta relativa se resuelve con respecto al directorio de trabajo de la sesión, o del subagente cuyo evento está gestionando el hook
  • $.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.
  • $.http.fetch: sigue hasta cinco redirecciones. En una redirección a un origen diferente, conserva solo los encabezados de solicitud accept, accept-language, content-type y user-agent que hayas establecido y descarta el resto, por lo que una solicitud que dependa de otro encabezado, como Authorization, puede fallar después de esa redirección. Los límites indican su tiempo de espera y los tamaños del cuerpo.

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.

Un mod puede rechazar tu llamada a $.process.spawn después de que el comando haya producido salida o haya terminado, y nada de lo que hizo el comando se deshace. En ese caso, la llamada se rechaza con un mensaje que termina con una de estas cadenas y el motivo del mod que la rechazó:

  • $.process.spawn started, and a plugin withheld its result:: el mod que la rechazó no había leído la salida del comando hasta el final. Claude Code detiene el comando si sigue en ejecución.
  • $.process.spawn ran, and a plugin withheld its result:: el mod que la rechazó había leído la salida del comando hasta el final, por lo que el comando ya había terminado

Lee toda la configuración, una fuente o una clave

$.settings.read() se resuelve en un objeto que contiene la configuración de todas las fuentes en uso, combinada según la precedencia. Pasa { source } para leer en cambio una sola fuente, { key } para leer una sola clave, o ambos:

Llamada Se resuelve en
$.settings.read() La configuración combinada como un solo objeto. Sus claves de nivel superior son las de un archivo settings.json, como permissions y hooks.
$.settings.read({ source: 'policy' }) La configuración de esa única fuente como un solo objeto, o {} cuando esa fuente no contiene configuración
$.settings.read({ key: 'spinnerTipsEnabled' }) El valor de esa clave en la configuración combinada, o undefined cuando la clave no está establecida
$.settings.read({ key: 'spinnerTipsEnabled', source: 'user' }) El valor de esa clave en esa única fuente, o undefined cuando esa fuente no la establece

Para source, pasa el nombre de la única fuente de configuración que se va a leer:

  • user: ~/.claude/settings.json
  • project: .claude/settings.json
  • local: .claude/settings.local.json
  • flag: lo que pasas con --settings, y la configuración en línea del SDK
  • policy: configuración administrada

Para key, pasa el nombre exacto de una clave de nivel superior, no una ruta con puntos hacia un objeto anidado. El argumento key requiere Claude Code v2.1.304 o posterior.

Próximos pasos