Building plugins
Hooks de Plugin
Los hooks de Plugin son puntos de extensión en proceso para los plugins de OpenClaw: permiten inspeccionar o cambiar las ejecuciones de agentes, las llamadas a herramientas, el flujo de mensajes, el ciclo de vida de las sesiones, el enrutamiento de subagentes, las instalaciones o el inicio del Gateway.
Use en su lugar los hooks internos para un pequeño script HOOK.md instalado por el operador
que reaccione a eventos de comandos y del Gateway, como /new,
/reset, /stop, agent:bootstrap o gateway:startup.
Inicio rápido
Registre hooks tipados con api.on(...) desde el punto de entrada del plugin:
export default definePluginEntry({ id: "tool-preflight", name: "Tool Preflight", register(api) { api.on( "before_tool_call", async (event) => { if (event.toolName !== "web_search") { return; } return { requireApproval: { title: "Run web search", description: `Allow search query: ${String(event.params.query ?? "")}`, severity: "info", timeoutMs: 60_000, }, }; }, { priority: 50 }, ); },});Los controladores que pueden devolver decisiones o modificaciones se ejecutan secuencialmente en
orden descendente de priority; los controladores con la misma prioridad mantienen el orden de registro.
Los controladores que solo observan se ejecutan en paralelo, y los envíos de observación
sin espera pueden solaparse con eventos posteriores. No use la prioridad para ordenar
los efectos secundarios de observación.
api.on(name, handler, opts?) acepta:
| Opción | Efecto |
|---|---|
priority |
Orden de ejecución; los valores más altos se ejecutan primero. |
timeoutMs |
Presupuesto de espera por hook. Cuando vence, OpenClaw deja de esperar a ese controlador y continúa. No cancela el controlador ni sus efectos secundarios. Omítalo para usar el tiempo de espera predeterminado por hook del ejecutor. |
Los operadores pueden establecer presupuestos para los hooks sin modificar el código del plugin:
{ "plugins": { "entries": { "my-plugin": { "hooks": { "timeoutMs": 30000, "timeouts": { "before_prompt_build": 90000, "agent_end": 60000 } } } } }}hooks.timeouts.<hookName> anula hooks.timeoutMs, que anula el valor
api.on(..., { timeoutMs }) definido por el plugin. Cada valor debe ser un
entero positivo de hasta 600000 ms. Se recomienda usar anulaciones por hook para los
hooks que se sabe que son lentos, de modo que un plugin no disponga de un presupuesto mayor en todas partes.
Una promesa de controlador cuyo tiempo de espera se ha agotado continúa ejecutándose porque las devoluciones de llamada de los hooks no reciben una señal de cancelación. El envío del hook puede liberar su admisión en el Gateway mientras el trabajo de ese plugin sigue en curso. Los plugins que sean propietarios de trabajos de larga duración deben proporcionar su propio ciclo de vida de cancelación y apagado.
Los hooks modificadores de salida message_sending y reply_payload_sending usan un valor
predeterminado de 15 segundos por controlador. Si uno agota el tiempo de espera, OpenClaw registra el error del plugin
y continúa con la carga útil más reciente para que la vía de entrega serializada pueda
finalizar. Establezca un presupuesto por hook mayor para los plugins que realizan intencionadamente
trabajos más lentos antes de la entrega.
Los plugins de canal que usan createReplyDispatcher también pueden declarar un presupuesto
positivo mayor por etapa con beforeDeliverOptions: { timeoutMs }, o al
añadir trabajo con dispatcher.appendBeforeDeliver(handler, { timeoutMs }).
Sin un presupuesto declarado por el propietario, esas devoluciones de llamada usan el mismo valor predeterminado de
15 segundos para que una devolución de llamada bloqueada no pueda retener la vía de entrega serializada.
Cada hook recibe event.context.pluginConfig, la configuración resuelta para el
plugin que registró ese controlador. OpenClaw la inyecta en cada controlador sin
modificar el objeto de evento compartido que ven otros plugins.
Catálogo de hooks
Los hooks se agrupan según la superficie que amplían. Los nombres en negrita aceptan un resultado de decisión (bloquear, cancelar, anular o requerir aprobación); los demás son solo de observación.
Turno del agente
| Hook | Propósito |
|---|---|
before_model_resolve |
Anular el proveedor o el modelo antes de cargar los mensajes de la sesión |
agent_turn_prepare |
Consumir las inserciones de turno en cola del plugin y añadir contexto al mismo turno antes de los hooks del prompt |
before_prompt_build |
Añadir contexto dinámico o texto al prompt del sistema antes de la llamada al modelo |
before_agent_run |
Inspeccionar el prompt final y los mensajes de la sesión antes de enviarlos al modelo; puede bloquear la ejecución |
before_agent_reply |
Omitir el turno del modelo mediante una respuesta sintética o silencio |
before_agent_finalize |
Inspeccionar la respuesta final natural y solicitar una ejecución adicional del modelo |
agent_end |
Observar los mensajes finales, el estado de éxito y la duración de la ejecución |
heartbeat_prompt_contribution |
Añadir contexto exclusivo de Heartbeat para plugins de supervisión en segundo plano y de ciclo de vida |
Observación de conversaciones
| Hook | Propósito |
|---|---|
model_call_started / model_call_ended |
Metadatos depurados de llamadas al proveedor/modelo: tiempos, resultado y hashes acotados de identificadores de solicitud. Sin contenido del prompt ni de la respuesta. |
llm_input |
Entrada del proveedor: prompt del sistema, prompt e historial |
llm_output |
Salida del proveedor, uso y el valor resuelto de contextTokenBudget cuando esté disponible |
Herramientas
| Hook | Propósito |
|---|---|
before_tool_call |
Reescribir los parámetros de la herramienta, bloquear la ejecución o requerir aprobación |
after_tool_call |
Observar los resultados y errores de la herramienta, y la duración |
resolve_exec_env |
Aportar variables de entorno propiedad del plugin a exec |
tool_result_persist |
Reescribir el mensaje del asistente generado a partir del resultado de una herramienta |
before_message_write |
Inspeccionar o bloquear la escritura de un mensaje en curso (poco frecuente) |
Mensajes y entrega
| Hook | Propósito |
|---|---|
inbound_claim |
Reclamar un mensaje entrante antes del enrutamiento al agente (respuestas sintéticas) |
channel_pairing_requested |
Observar las solicitudes de emparejamiento de mensajes directos recién creadas |
message_received |
Observar el contenido entrante, el remitente, el hilo y los metadatos |
message_sending |
Reescribir el contenido saliente o cancelar la entrega |
reply_payload_sending |
Modificar o cancelar las cargas útiles de respuesta normalizadas antes de la entrega |
message_sent |
Observar el éxito o el fallo de la entrega saliente |
before_dispatch |
Inspeccionar o reescribir un envío saliente antes de transferirlo al canal |
reply_dispatch |
Participar en el Pipeline final de envío de respuestas |
Sesiones y Compaction
| Hook | Propósito |
|---|---|
session_start / session_end |
Realizar un seguimiento de los límites del ciclo de vida de las sesiones. reason es uno de new, reset, idle, daily, compaction, deleted, shutdown, restart o unknown. shutdown/restart se activan desde el finalizador de apagado del Gateway cuando el proceso se detiene o reinicia con sesiones activas, para que los plugins (memoria, almacenes de transcripciones) puedan finalizar las filas fantasma en lugar de dejarlas abiertas entre reinicios. El finalizador está limitado para que un plugin lento no pueda bloquear SIGTERM/SIGINT. |
before_compaction / after_compaction |
Observar o anotar los ciclos de Compaction |
before_reset |
Observar eventos de restablecimiento de sesión (/reset, restablecimientos mediante programación) |
Para las llamadas a sessions.create con parentSessionKey y emitCommandHooks: true, un elemento secundario distinto siempre recibe session_start. Los llamadores declaran si el elemento principal también recibe el estado terminal session_end con succeedsParent: true significa sucesor, false significa elemento secundario paralelo. Si se omite, se conserva el comportamiento heredado de transición del elemento principal. Los hooks command:new y before_reset siguen describiendo la acción /new solicitada en ambos casos.
Subagentes
subagent_spawned/subagent_ended- observa el inicio y la finalización del subagente.subagent_delivery_target- mecanismo de compatibilidad para entregar la finalización cuando ningún enlace de sesión del núcleo puede proyectar una ruta.subagent_spawning- mecanismo de compatibilidad obsoleto. Ahora el núcleo prepara los enlaces de subagentesthread: truemediante adaptadores de enlace de sesión del canal antes de que se activesubagent_spawned.subagent_spawnedincluyeresolvedModelyresolvedProvidercuando OpenClaw ha resuelto el modelo nativo de la sesión secundaria antes del inicio.subagent_endedcontienetargetSessionKey(identidad; coincide consubagent_spawned.childSessionKey),targetKind("subagent"o"acp"),reason, eloutcomeopcional ("ok","error","timeout","killed","reset"o"deleted"), elerroropcional,runId,endedAt,accountIdysendFarewell. No incluyeagentIdnichildSessionKey; usetargetSessionKeypara correlacionarlo con el eventosubagent_spawnedcorrespondiente.
Ciclo de vida
| Mecanismo | Propósito |
|---|---|
gateway_start / gateway_stop |
Iniciar o detener servicios propiedad del plugin junto con el Gateway |
deactivate |
Alias de compatibilidad obsoleto para gateway_stop; use gateway_stop en plugins nuevos |
cron_reconciled |
Conciliar con el estado completo de Cron del Gateway después del inicio o de una recarga |
cron_changed |
Observar cambios en el ciclo de vida de Cron propiedad del Gateway (añadido, actualizado, eliminado, iniciado, finalizado, programado) |
before_install |
Inspeccionar material preparado para instalar Skills o plugins desde un entorno de ejecución de plugin cargado |
Solicitudes de vinculación de canales
Use channel_pairing_requested cuando un plugin necesite notificar a un operador o
escribir un registro de auditoría después de que un remitente de MD no vinculado cree una solicitud
de vinculación pendiente. El mecanismo se activa cuando se crea la solicitud; la entrega por el canal de
la respuesta de vinculación no se retrasa por controladores de mecanismos lentos o con errores.
api.on("channel_pairing_requested", async (event) => { await notifyOperator({ text: `Nueva solicitud de vinculación de ${event.channel} de ${event.senderId}: ${event.code}`, });});El mecanismo es solo de observación. No aprueba, rechaza, suprime ni reescribe
la respuesta de vinculación. La carga útil incluye el canal, el accountId opcional,
el senderId con ámbito de canal, el code de vinculación y los metadatos del canal. Trate el
código de vinculación como una credencial de aprobación activa y de un solo uso, y entréguelo únicamente a un
destino de operador de confianza. Trate metadata como texto de identidad no confiable
proporcionado por el remitente. El mecanismo no incluye el cuerpo ni los archivos multimedia del mensaje entrante.
Mecanismos de depuración del entorno de ejecución
Use before_model_resolve para cambiar de proveedor o modelo durante un turno del agente; se
ejecuta antes de resolver el modelo. llm_output solo se ejecuta después de que un intento del modelo
produce una salida del asistente.
Para comprobar el modelo efectivo de la sesión, inspeccione los registros del entorno de ejecución y, después,
use openclaw sessions o las superficies de sesión/estado del Gateway. Para depurar
las cargas útiles del proveedor, inicie el Gateway con --raw-stream y
--raw-stream-path <path> para escribir los eventos sin procesar del flujo del modelo en un archivo jsonl.
Política de llamadas a herramientas
before_tool_call recibe:
event.toolNameevent.params- los
event.toolKindyevent.toolInputKindopcionales, discriminadores con autoridad del host para herramientas que comparten nombres intencionadamente; por ejemplo, las llamadas externas deexecen modo de código usantoolKind: "code_mode_exec"e incluyentoolInputKind: "javascript" | "typescript"cuando se conoce el lenguaje de entrada - el
event.derivedPathsopcional, indicaciones de rutas de destino derivadas por el host según el mejor esfuerzo para envoltorios de herramientas conocidos, comoapply_patch; estas rutas pueden estar incompletas o sobreestimar lo que la herramienta tocará realmente (por ejemplo, con entradas mal formadas o parciales) - el
event.runIdopcional - el
event.toolCallIdopcional - campos de contexto como
ctx.agentId,ctx.sessionKey,ctx.sessionId,ctx.runId,ctx.toolKind,ctx.toolInputKindy elctx.tracede diagnóstico - el
ctx.requesteropcional, el solicitante derivado por el host que inició la ejecución del mensaje actual. Puede incluirchannel,accountId,senderId,senderIsOwnery elroleIdsnativo del proveedor. Los campos ausentes no están demostrados, no constituyen garantías falsas; deniegue de forma predeterminada cuando la política los requiera.
Puede devolver:
type BeforeToolCallResult = { params?: Record<string, unknown>; block?: boolean; blockReason?: string; requireApproval?: { title: string; description: string; severity?: "info" | "warning" | "critical"; timeoutMs?: number; /** @deprecated Las aprobaciones no resueltas siempre se deniegan. */ timeoutBehavior?: "allow" | "deny"; allowedDecisions?: Array<"allow-once" | "allow-always" | "deny">; pluginId?: string; onResolution?: ( decision: "allow-once" | "allow-always" | "deny" | "timeout" | "cancelled", ) => Promise<void> | void; };};Comportamiento de protección para mecanismos de ciclo de vida tipados:
block: truees terminal y omite los controladores de menor prioridad.block: falsese trata como si no hubiera decisión.paramsreescribe los parámetros de la herramienta para su ejecución.requireApprovalpausa la ejecución del agente y solicita la intervención del usuario mediante las aprobaciones de plugins./approvepuede aprobar tanto las aprobaciones de ejecución como las de plugins. En los retransmisoresPreToolUsenativos del modo de informe del servidor de aplicaciones de Codex, esto se delega en la solicitud de aprobación correspondiente del servidor de aplicaciones; consulte Entorno de ejecución del arnés de Codex.- Un
block: truede menor prioridad aún puede bloquear después de que un mecanismo de mayor prioridad haya solicitado aprobación. onResolutionrecibe la decisión resuelta:allow-once,allow-always,deny,timeoutocancelled.
Política consciente del remitente en un solo archivo
Un archivo de plugin independiente puede mantener en el código una política específica de la implementación
en lugar de añadir otro esquema de configuración. Este ejemplo proporciona a los propietarios todas las herramientas,
permite que los mantenedores configurados usen un conjunto conservador de herramientas y acciones de mensajes,
y expone /fix a los remitentes ya autorizados por la configuración del canal:
const AGENT_ID = "maintenance-agent";const MAINTAINER_SCOPES = [ { channel: "discord", accountId: "operations", senderIds: new Set(["maintainer-user-id"]), roleIds: new Set(["maintainer-role-id"]), },];const MAINTAINER_TOOLS = new Set(["read", "web_fetch", "web_search", "session_status", "message"]);const MAINTAINER_MESSAGE_ACTIONS = new Set(["react", "reply", "thread-create", "thread-reply"]); export default definePluginEntry({ id: "maintenance-access", name: "Acceso de mantenimiento", description: "Aplicar una política de herramientas consciente del remitente al agente de mantenimiento.", register(api) { api.on("before_tool_call", (event, ctx) => { if (ctx.agentId !== AGENT_ID) { return; } const requester = ctx.requester; if (requester?.senderIsOwner === true) { return; } const maintainerScope = requester ? MAINTAINER_SCOPES.find( (scope) => scope.channel === requester.channel && scope.accountId === requester.accountId, ) : undefined; const isMaintainer = maintainerScope !== undefined && ((requester?.senderId !== undefined && maintainerScope.senderIds.has(requester.senderId)) || requester?.roleIds?.some((roleId) => maintainerScope.roleIds.has(roleId)) === true); if (!isMaintainer) { return { block: true, blockReason: "Se requiere acceso de mantenedor." }; } if (event.toolName === "message") { const action = typeof event.params.action === "string" ? event.params.action : ""; if (MAINTAINER_MESSAGE_ACTIONS.has(action)) { return; } return { block: true, blockReason: `Se requiere ser propietario para message.${action || "unknown"}.` }; } if (MAINTAINER_TOOLS.has(event.toolName)) { return; } return { block: true, blockReason: `Se requiere ser propietario para ${event.toolName}.` }; }); api.registerCommand({ name: "fix", description: "Solicitar al agente de mantenimiento que investigue y corrija un problema.", acceptsArgs: true, requireAuth: true, handler: async (ctx) => ctx.agentId === AGENT_ID ? { continueAgent: true } : { text: "Este comando solo está disponible en la conversación de mantenimiento." }, }); },});Cargue el archivo directamente y reinicie el Gateway:
{ agents: { list: [ { id: "maintenance-agent", workspace: "~/.openclaw/workspace-maintenance", }, ], }, bindings: [ { agentId: "maintenance-agent", match: { channel: "discord", accountId: "operations", peer: { kind: "channel", id: "maintenance-channel-id" }, }, }, ], plugins: { load: { paths: ["~/.openclaw/policies/maintenance-access.ts"] }, },}AGENT_ID debe indicar el agente enlazado a la conversación de mantenimiento. El
enlace selecciona ese agente para los mensajes normales y /fix; el archivo independiente
sigue siendo el único propietario de la política de herramientas para propietarios y mantenedores.
requireAuth: true reutiliza la admisión de remitentes existente de cada canal. En
Discord, una lista de permitidos users/roles de un servidor o canal puede autorizar a la
audiencia de mantenimiento. Otros canales pueden usar identificadores de remitente estables. Después, el mecanismo
aplica la decisión más específica por herramienta en cada llamada a herramientas de la ejecución, incluidas
las llamadas nativas PreToolUse de Codex. Puede vetar una herramienta que el modelo puede ver, pero no
puede añadir una herramienta omitida por el host. Las políticas existentes del entorno aislado, de aprobación
de ejecución, de herramientas del núcleo exclusivas del propietario y de canales siguen aplicándose; el mecanismo no puede eludirlas.
Limite los identificadores de remitentes y roles a un par exacto de canal/cuenta como se muestra; ambos son
espacios de nombres locales del proveedor. Mantenga conservadoras las listas de permitidos. Añada herramientas de
escritura o ejecución únicamente cuando el entorno aislado y la política de aprobación de la implementación hagan
que sea seguro. Para ejecuciones automatizadas o del sistema, decida explícitamente si la ausencia de
ctx.requester debe permitirse; el ejemplo la deniega para el agente con ese ámbito.
Consulte Solicitudes de permisos de plugins para obtener información sobre
el enrutamiento de aprobaciones, el comportamiento de las decisiones y cuándo usar requireApproval en lugar
de herramientas opcionales o aprobaciones de ejecución.
Los plugins que necesiten una política a nivel del host pueden registrar políticas de herramientas de confianza con
api.registerTrustedToolPolicy(...). Estas se ejecutan antes que los mecanismos
before_tool_call ordinarios y antes que las decisiones normales de los mecanismos. Las políticas de confianza
incluidas se ejecutan primero; las políticas de confianza de plugins instalados se ejecutan después según el orden
de carga de los plugins; los mecanismos before_tool_call ordinarios se ejecutan a continuación. Los plugins incluidos
mantienen la ruta de políticas de confianza existente. Los plugins instalados deben habilitarse explícitamente
y declarar cada identificador de política en contracts.trustedToolPolicies; los identificadores no declarados
se rechazan antes del registro. Los identificadores de políticas tienen el ámbito del plugin que los registra,
por lo que distintos plugins pueden reutilizar el mismo identificador local. Use este nivel únicamente
para controles de confianza del host, como políticas del espacio de trabajo, aplicación de presupuestos o
seguridad de flujos de trabajo reservados.
Hook del entorno de ejecución
resolve_exec_env permite que los plugins aporten variables de entorno a las invocaciones de herramientas exec
antes de que se ejecute el comando. Recibe:
event.sessionKeyevent.toolName, actualmente siempre"exec"event.host, uno de"gateway","sandbox"o"node"- campos de contexto como
ctx.agentId,ctx.sessionKey,ctx.messageProvideryctx.channelId
Devuelva un Record<string, string> para fusionarlo con el entorno de ejecución. Los controladores
se ejecutan por orden de prioridad; para una misma clave, los resultados posteriores
sobrescriben los anteriores.
La salida del hook se filtra mediante la política de claves del entorno de ejecución del host antes de
fusionarse. PATH siempre se descarta (la resolución de comandos y las comprobaciones de binarios seguros
dependen de ello). Se descartan las claves no válidas y las claves peligrosas que sobrescriben valores del host, como LD_*,
DYLD_*, NODE_OPTIONS, las variables de proxy (HTTP_PROXY, HTTPS_PROXY,
ALL_PROXY, NO_PROXY) y las variables que sobrescriben TLS (NODE_TLS_REJECT_UNAUTHORIZED,
SSL_CERT_FILE y similares). El entorno filtrado del plugin se incluye
en los metadatos de aprobación y auditoría del Gateway y se reenvía a las solicitudes
de ejecución del host del nodo.
Persistencia de los resultados de herramientas
Los resultados de herramientas pueden incluir details estructurados para el renderizado de la interfaz de usuario, diagnósticos,
enrutamiento de medios o metadatos propiedad del plugin. Trate details como metadatos de ejecución,
no como contenido del prompt:
- OpenClaw elimina
toolResult.detailsantes de la repetición del proveedor y de la entrada de Compaction para que los metadatos no se conviertan en contexto del modelo. - Las entradas de sesión persistentes conservan solo
detailsacotados. Los detalles de tamaño excesivo se sustituyen por un resumen compacto ypersistedDetailsTruncated: true. tool_result_persistybefore_message_writese ejecutan antes del límite final de persistencia. Mantenga pequeños losdetailsdevueltos y evite colocar texto relevante para el prompt únicamente endetails; coloque la salida de herramienta visible para el modelo encontent.
Hooks de prompt y modelo
Utilice los hooks específicos de cada fase para los plugins nuevos:
before_model_resolve: recibe únicamente el prompt actual y los metadatos de los archivos adjuntos. DevuelvaproviderOverrideomodelOverride.agent_turn_prepare: recibe el prompt actual, los mensajes de sesión preparados y cualquier inyección en cola de ejecución única extraída para esta sesión. DevuelvaprependContextoappendContext.before_prompt_build: recibe el prompt actual y los mensajes de sesión. DevuelvaprependContext,appendContext,systemPrompt,prependSystemContextoappendSystemContext.heartbeat_prompt_contribution: se ejecuta únicamente en turnos de Heartbeat y devuelveprependContextoappendContext. Está destinado a monitores en segundo plano que necesitan resumir el estado actual sin cambiar los turnos iniciados por el usuario.
before_agent_run se ejecuta después de construir el prompt y antes de cualquier entrada del modelo,
incluida la carga de imágenes locales del prompt y la observación de llm_input. Recibe
la entrada actual del usuario como prompt, además del historial de sesión cargado en messages
y el prompt de sistema activo. Devuelva { outcome: "block", reason, message? }
para detener la ejecución antes de que el modelo lea el prompt. reason es interno;
message es el texto de sustitución mostrado al usuario. Solo se admiten los resultados pass y block;
las formas de decisión no admitidas producen un cierre seguro.
Cuando se bloquea una ejecución, OpenClaw almacena únicamente el texto de sustitución en
message.content, junto con metadatos de bloqueo no sensibles, como el identificador
del plugin bloqueador y la marca de tiempo. El texto original del usuario no se conserva en la transcripción
ni en el contexto futuro. Los motivos internos del bloqueo se consideran sensibles y
se excluyen de las cargas útiles de transcripción, historial, difusión, registro y diagnóstico.
La observabilidad debe utilizar campos depurados, como el identificador del bloqueador, el resultado,
la marca de tiempo o una categoría segura.
Los hooks de turno del agente, incluido agent_end, incluyen event.runId cuando OpenClaw puede
identificar la ejecución activa; el mismo valor también está en ctx.runId. Las ejecuciones impulsadas por
Cron también exponen ctx.jobId (el identificador del trabajo Cron de origen) en el contexto del turno del agente
para que los hooks puedan limitar las métricas, los efectos secundarios o el estado a un trabajo
programado específico. ctx.jobId no forma parte del contexto de herramienta before_tool_call.
Para las ejecuciones originadas en un canal, ctx.channel y ctx.messageProvider identifican
la superficie del proveedor, como discord o telegram, mientras que ctx.channelId es
el identificador de destino de la conversación cuando OpenClaw puede derivarlo de la
clave de sesión o de los metadatos de entrega.
Cuando está disponible la identidad del remitente, los contextos de hooks del agente también incluyen:
ctx.senderId- identificador del remitente limitado al canal (p. ej.,open_idde Feishu, identificador de usuario de Discord). Se rellena cuando la ejecución se origina a partir de un mensaje de usuario con metadatos conocidos del remitente.ctx.chatId- identificador de conversación nativo del transporte (p. ej.,chat_idde Feishu,chat_idde Telegram). Se rellena cuando el canal de origen proporciona un identificador de conversación nativo.ctx.channelContext.sender.id- el mismo identificador de remitente quectx.senderId, dentro de un objeto propiedad del canal que los plugins pueden ampliar con campos específicos del canal.ctx.channelContext.chat.id- el mismo identificador de conversación quectx.chatId, dentro de un objeto propiedad del canal que los plugins pueden ampliar con campos específicos del canal.
El núcleo solo define los campos id anidados. Los plugins de canal que transfieren metadatos más completos
del remitente o del chat mediante el auxiliar de entrada pueden ampliar
PluginHookChannelSenderContext o PluginHookChannelChatContext desde
openclaw/plugin-sdk/channel-inbound:
declare module "openclaw/plugin-sdk/channel-inbound" { interface PluginHookChannelSenderContext { unionId?: string; userId?: string; }}Los plugins de canal transfieren esos campos mediante el auxiliar del SDK de entrada:
buildChannelInboundEventContext({ // ... channelContext: { sender: { id: senderOpenId, unionId, userId }, chat: { id: chatId }, },});Estos campos son opcionales y no están presentes en las ejecuciones originadas por el sistema (Heartbeat, Cron, evento de ejecución).
ctx.senderExternalId se mantiene como campo obsoleto de compatibilidad con el código fuente para
plugins antiguos. El núcleo no lo rellena; las nuevas identidades de remitente
específicas del canal deben residir en ctx.channelContext.sender mediante la ampliación
del módulo.
agent_end es un hook de observación. Las rutas del Gateway y del arnés persistente lo ejecutan
sin esperar su resultado después del turno, mientras que las rutas CLI efímeras de ejecución única esperan
la promesa del hook antes de limpiar el proceso, de modo que los plugins de confianza puedan volcar
la observabilidad del terminal o capturar el estado. El ejecutor de hooks aplica un tiempo de espera de 30 segundos
para que un plugin bloqueado o un endpoint de incrustación no pueda dejar la promesa del hook
pendiente indefinidamente. Se registra el tiempo de espera y OpenClaw continúa; no
cancela el trabajo de red propiedad del plugin, salvo que este también utilice su propia señal
de anulación.
Utilice model_call_started y model_call_ended para la telemetría de llamadas al proveedor
que no deba recibir prompts sin procesar, historial, respuestas, encabezados, cuerpos
de solicitudes ni identificadores de solicitudes del proveedor. Estos hooks incluyen metadatos estables como
runId, callId, provider, model, los valores opcionales api/transport, los valores terminales
durationMs/outcome y upstreamRequestIdHash cuando OpenClaw puede derivar un
hash acotado del identificador de solicitud del proveedor. Cuando el entorno de ejecución ha resuelto
los metadatos de la ventana de contexto, el evento y el contexto del hook también incluyen
contextTokenBudget, el presupuesto efectivo de tokens después de los límites del modelo, la configuración y el agente,
además de contextWindowSource y contextWindowReferenceTokens cuando se ha aplicado
un límite inferior.
before_agent_finalize se ejecuta únicamente cuando un arnés está a punto de aceptar una respuesta
final natural del asistente. No es la ruta de cancelación /stop y no
se ejecuta cuando el usuario anula un turno. Devuelva { action: "revise", reason } para solicitar
al arnés una pasada adicional del modelo antes de finalizar, { action: "finalize", reason? } para forzar la finalización u omita el resultado para continuar.
Los controladores tienen un presupuesto predeterminado de 15 s; si se agota el tiempo de espera, OpenClaw registra el fallo y
continúa con la respuesta final original.
Los hooks nativos Stop de Codex se retransmiten a este hook como decisiones
before_agent_finalize de OpenClaw.
Al devolver action: "revise", los plugins pueden incluir metadatos retry para
que la pasada adicional del modelo sea acotada y segura para la repetición:
type BeforeAgentFinalizeRetry = { instruction: string; idempotencyKey?: string; maxAttempts?: number;};instruction se añade al motivo de revisión enviado al arnés.
idempotencyKey permite al host contar los reintentos de la misma solicitud del plugin
entre decisiones de finalización equivalentes, y maxAttempts limita cuántas pasadas
adicionales permitirá el host antes de continuar con la respuesta final natural.
Los plugins no incluidos en el paquete que necesiten hooks de conversación sin procesar (before_model_resolve,
before_agent_reply, llm_input, llm_output, before_agent_finalize,
agent_end o before_agent_run) deben establecer:
{ "plugins": { "entries": { "my-plugin": { "hooks": { "allowConversationAccess": true } } } }}Los hooks que modifican el prompt y las inyecciones duraderas para el siguiente turno pueden desactivarse por
plugin con plugins.entries.<id>.hooks.allowPromptInjection=false.
Extensiones de sesión e inyecciones para el siguiente turno
Los plugins de flujo de trabajo pueden conservar un estado de sesión pequeño compatible con JSON mediante
api.session.state.registerSessionExtension(...) y actualizarlo mediante el método
sessions.pluginPatch del Gateway. Las filas de sesión proyectan el estado de extensión
registrado mediante pluginExtensions, lo que permite que Control UI y otros
clientes rendericen el estado propiedad del plugin sin conocer sus detalles internos.
api.registerSessionExtension(...) sigue funcionando, pero está obsoleto en favor del
espacio de nombres api.session.state.
Utilice api.session.workflow.enqueueNextTurnInjection(...) cuando un plugin necesite
que un contexto duradero llegue exactamente una vez al siguiente turno del modelo (el valor de nivel superior
api.enqueueNextTurnInjection(...) es un alias obsoleto con el mismo
comportamiento). OpenClaw extrae las inyecciones en cola antes de los hooks del prompt, descarta
las inyecciones caducadas y elimina duplicados por idempotencyKey para cada plugin. Esta es
la interfaz adecuada para reanudar aprobaciones, resúmenes de políticas, diferencias de monitores en segundo plano
y continuaciones de comandos que deban ser visibles para el modelo en el
siguiente turno, pero que no deban convertirse en texto permanente del prompt de sistema.
La semántica de limpieza forma parte del contrato. Las devoluciones de llamada de limpieza de extensiones de sesión y
del ciclo de vida del entorno de ejecución reciben reset, delete, disable o
restart. El host elimina el estado persistente de las extensiones de sesión
y las inyecciones pendientes para el siguiente turno del plugin propietario al restablecer, eliminar o deshabilitar; el reinicio
conserva el estado duradero de la sesión, mientras que las devoluciones de llamada de limpieza permiten que los plugins liberen
trabajos del planificador, contexto de ejecución y otros recursos fuera de banda de la generación
anterior del entorno de ejecución.
Hooks de mensajes
Utilice los hooks de mensajes para el enrutamiento y la política de entrega en el nivel del canal:
message_received: observa el contenido entrante, el remitente,threadId,messageId,senderId, la correlación opcional de ejecución/sesión, losmediaordenados y los metadatos.message_sending: reescribecontento devuelve{ cancel: true }.reply_payload_sending: reescribe objetosReplyPayloadnormalizados (incluidospresentation,delivery, referencias de medios y texto) o devuelve{ cancel: true }.message_sent: observa el éxito o el fallo finales.
En las respuestas TTS únicamente de audio, content puede contener la transcripción hablada
oculta, aunque la carga útil del canal no tenga texto ni subtítulo visibles.
La reescritura de ese content solo actualiza la transcripción visible para el hook; no se
renderiza como subtítulo del contenido multimedia.
Los eventos reply_payload_sending pueden incluir usageState, una instantánea activa
aproximada por turno del modelo, el uso y el contexto. La entrega duradera, la repetición recuperada y
las respuestas sin una correlación exacta de ejecución lo omiten.
Los contextos de los hooks de mensajes exponen campos de correlación estables cuando están disponibles:
ctx.sessionKey, ctx.runId, ctx.messageId, ctx.senderId, ctx.trace,
ctx.traceId, ctx.spanId, ctx.parentSpanId y ctx.callDepth. Los contextos entrantes
y before_dispatch también exponen metadatos de respuesta cuando el canal
dispone de datos de mensajes citados filtrados por visibilidad: replyToId, replyToIdFull,
replyToBody, replyToSender y replyToIsQuote. Se deben priorizar estos
campos de primera clase antes de leer los metadatos heredados.
Se deben priorizar los campos tipados threadId y replyToId antes de usar metadatos
específicos del canal.
Los eventos de reclamación entrante y de mensaje recibido exponen media?: PluginHookMediaFact[] como la API canónica de archivos adjuntos. Cada dato puede incluir
path, url, contentType, kind, transcribed, messageId y
workspaceDir; la posición en el array constituye la identidad del archivo adjunto. Cuando un archivo adjunto remoto
todavía no se ha preparado localmente, se omite media,
mediaStagingPending: true, y originalMedia contiene los
datos del proveedor. No se debe considerar originalMedia.path como legible localmente hasta que un evento
posterior de preparación proporcione media.
Las propiedades de metadatos en singular/plural mediaPath, mediaUrl, mediaType, mediaPaths,
mediaUrls, mediaTypes y las correspondientes originalMedia* son
alias de compatibilidad obsoletos. Los hooks nuevos deben usar los arrays tipados
de nivel superior.
Reglas de decisión:
message_sendingconcancel: truees terminal.message_sendingconcancel: falsese considera que no toma ninguna decisión.- El
contentreescrito continúa hacia los hooks de menor prioridad, salvo que un hook posterior cancele la entrega. reply_payload_sendingse ejecuta después de normalizar la carga útil y antes de la entrega al canal, incluidas las respuestas enrutadas de vuelta al canal de origen. Los controladores se ejecutan secuencialmente y cada uno recibe la carga útil más reciente generada por los controladores de mayor prioridad.- Las cargas útiles de
reply_payload_sendingno exponen marcadores de confianza del entorno de ejecución, comotrustedLocalMedia; los plugins pueden editar la estructura de la carga útil, pero no pueden conceder confianza local a los archivos multimedia. message_sendingpuede devolvercancelReasonymetadataacotado junto con una cancelación. Las API nuevas del ciclo de vida de los mensajes exponen esto como un resultado de entrega suprimida con el motivocancelled_by_message_sending_hook; por compatibilidad, la entrega directa heredada sigue devolviendo un array de resultados vacío.message_sentes solo de observación. Los fallos de los controladores se registran y no modifican el resultado de la entrega.
Hooks de instalación
Se debe usar security.installPolicy para las decisiones de permitir o bloquear que correspondan al operador. Esa
política se ejecuta desde la configuración de OpenClaw, abarca las rutas de instalación y actualización de la CLI y
bloquea de forma segura cuando está habilitada pero no disponible.
before_install es un hook del ciclo de vida del entorno de ejecución de plugins. Se ejecuta después de
security.installPolicy únicamente en el proceso de OpenClaw en el que los hooks de plugins
ya se hayan cargado, como en los flujos de instalación respaldados por el Gateway. Resulta útil para
observaciones, advertencias y comprobaciones de compatibilidad que correspondan al plugin, pero no es
el límite principal de seguridad empresarial o del host para las instalaciones. El campo
builtinScan permanece en la carga útil del evento por compatibilidad, pero
OpenClaw ya no ejecuta el bloqueo integrado de código peligroso durante la instalación, por lo que
es un resultado ok vacío. Se pueden devolver hallazgos adicionales o
{ block: true, blockReason } para detener la instalación en ese proceso.
block: true es terminal. block: false se considera que no toma ninguna decisión. Los fallos de los controladores
bloquean la instalación de forma segura.
Ciclo de vida del Gateway
Se debe usar gateway_start para iniciar servicios generales de plugins y gateway_stop para
liberar recursos de larga duración. El planificador de Cron aún puede estar cargándose cuando
se ejecuta gateway_start, por lo que no debe usarse como señal de referencia para una proyección
externa de Cron.
No se debe depender del hook interno gateway:startup para los servicios del entorno de ejecución
que correspondan al plugin.
cron_reconciled se activa después de que el planificador de Cron del Gateway y sus observadores
de salida hayan conciliado su estado persistente. Se activa tanto en el inicio
inicial como al sustituir el planificador durante una recarga de la configuración. El evento informa de
reason (startup o reload) y del estado efectivo de enabled. Aunque Cron esté deshabilitado,
se sigue emitiendo con enabled: false, lo que permite que una proyección externa
elimine activaciones obsoletas. Se debe usar ctx.getCron?.() para la instancia exacta del planificador que
completó la conciliación; una recarga posterior no redirige esa devolución de llamada.
ctx.abortSignal corresponde a esa misma instantánea del planificador. El Gateway la cancela en cuanto
se activa un planificador más reciente o comienza el apagado. Se debe transmitir a cada
efecto secundario persistente y no aceptar la instantánea después de su cancelación.
Esta es una señal del ciclo de vida del planificador, no una señal de activación del plugin:
una recarga en caliente exclusiva del plugin no vuelve a emitirla. Un consumidor recién habilitado recibe
su primera referencia en la siguiente sustitución del planificador o inicio del Gateway.
Al igual que otros hooks de observación, las devoluciones de llamada de gateway_start y cron_reconciled
pueden solaparse. Si ambos controladores comparten la inicialización del plugin, deben coordinarse
mediante una promesa de disponibilidad local del plugin en lugar de depender del orden de las devoluciones de llamada.
cron_changed se activa para los eventos del ciclo de vida de Cron gestionados por el Gateway con una carga útil
de evento tipada que abarca los motivos added, updated, removed, started, finished
y scheduled. El evento incluye una instantánea de PluginHookGatewayCronJob
(incluidos state.nextRunAtMs, state.lastRunStatus y
state.lastError cuando están presentes), además de un PluginHookGatewayCronDeliveryStatus
de not-requested | delivered | not-delivered | unknown. Los eventos de eliminación
son posteriores a la confirmación: solo se activan después de que la eliminación persistente se complete correctamente y siguen incluyendo
la instantánea de la tarea eliminada para que los planificadores externos puedan conciliar el estado.
Un evento scheduled es posterior a la confirmación: solo se activa después de que una escritura persistente
correcta modifique el nextRunAtMs efectivo de una tarea existente, excluyendo el evento explícito del ciclo de vida
added, updated o removed de esa tarea. El valor event.nextRunAtMs
de nivel superior es la siguiente activación confirmada; cuando no está presente, la tarea
no tiene una próxima activación. Estos eventos deben tratarse como indicios de conciliación, no como un registro
ordenado de cambios. Deben usarse como indicios combinables para volver a leer el planificador capturado por última vez por
cron_reconciled; no se debe adoptar el planificador de un contexto cron_changed.
OpenClaw debe mantenerse como fuente de verdad para las comprobaciones de vencimiento y la ejecución.
Proyección externa segura de Cron
Se debe proyectar una instantánea completa de activaciones en lugar de reenviar los cambios de los eventos de Cron. La
operación replaceAll del adaptador externo debe ser atómica e idempotente, y debe
resolverse únicamente después de que el host haya aceptado la instantánea de forma persistente. También debe
respetar la señal de cancelación proporcionada: si la señal se cancela antes de la
aceptación persistente, el adaptador no debe aceptar esa instantánea.
Este patrón mantiene en ejecución un único trabajador con el estado más reciente. Solo cron_reconciled
adopta una instancia del planificador; cron_changed únicamente solicita a ese trabajador que vuelva a leer
la instancia autoritativa, por lo que un indicio tardío no puede restaurar un planificador anterior.
Una revisión más reciente cancela el intento activo del host antes de que pueda aceptar una
instantánea obsoleta.
type ExternalWake = { jobId: string; runAtMs: number }; type ExternalWakeHost = { replaceAll(wakes: readonly ExternalWake[], options: { signal: AbortSignal }): Promise<void>; close(): Promise<void>;}; type CronReader = { list(options: { includeDisabled: true }): Promise< Array<{ id: string; enabled?: boolean; state?: { nextRunAtMs?: number }; }> >;}; export function registerCronProjection(api: OpenClawPluginApi, host: ExternalWakeHost) { const lifecycle = new AbortController(); let cron: CronReader | undefined; let enabled = false; let hasBaseline = false; let reconciliationSignal: AbortSignal | undefined; let requestedRevision = 0; let appliedRevision = 0; let worker = Promise.resolve(); let activeAttempt: AbortController | undefined; const projectLatest = async () => { let retryMs = 1_000; while (!lifecycle.signal.aborted && appliedRevision < requestedRevision) { const ownerSignal = reconciliationSignal; if (!ownerSignal || ownerSignal.aborted) { return; } const targetRevision = requestedRevision; const attempt = new AbortController(); const signal = AbortSignal.any([lifecycle.signal, ownerSignal, attempt.signal]); activeAttempt = attempt; try { const jobs = enabled && cron ? await cron.list({ includeDisabled: true }) : []; if (signal.aborted || targetRevision !== requestedRevision) { continue; } const wakes = jobs .flatMap((job): ExternalWake[] => { const runAtMs = job.enabled === false ? undefined : job.state?.nextRunAtMs; return runAtMs === undefined ? [] : [{ jobId: job.id, runAtMs }]; }) .sort((a, b) => a.runAtMs - b.runAtMs || a.jobId.localeCompare(b.jobId)); await host.replaceAll(wakes, { signal }); if (signal.aborted || targetRevision !== requestedRevision) { continue; } appliedRevision = targetRevision; retryMs = 1_000; } catch { if (lifecycle.signal.aborted || ownerSignal.aborted) { return; } if (attempt.signal.aborted) { continue; } api.logger.warn(`external cron projection failed; retrying in ${retryMs}ms`); try { await sleep(retryMs, undefined, { signal }); } catch { if (lifecycle.signal.aborted) { return; } if (attempt.signal.aborted) { continue; } } retryMs = Math.min(retryMs * 2, 30_000); } finally { if (activeAttempt === attempt) { activeAttempt = undefined; } } } }; const requestProjection = () => { const targetRevision = ++requestedRevision; activeAttempt?.abort(); worker = worker.then(async () => { if (!lifecycle.signal.aborted && appliedRevision < targetRevision) { await projectLatest(); } }); return worker; }; api.on("cron_reconciled", (event, ctx) => { const reconciledCron = ctx.getCron?.(); if (event.enabled && !reconciledCron) { api.logger.warn("cron reconciliation did not expose a scheduler"); return; } cron = reconciledCron; enabled = event.enabled; hasBaseline = true; reconciliationSignal = ctx.abortSignal; return requestProjection(); }); api.on("cron_changed", () => { if (hasBaseline) { return requestProjection(); } }); api.on("gateway_stop", async () => { lifecycle.abort(); await worker; await host.close(); });}Cuando cron_reconciled informa de enabled: false, la misma ruta llama a
replaceAll([]) y elimina las activaciones externas obsoletas. Los reintentos y la espera progresiva de este ejemplo
son locales al proceso y consideran transitorios los fallos del adaptador del entorno de ejecución; la configuración
no reintentable debe validarse antes del registro. OpenClaw no proporciona un
buzón de salida para los efectos de los hooks de plugins. Si el proceso termina antes de la aceptación persistente,
el siguiente inicio del Gateway emite una nueva instantánea autoritativa de cron_reconciled.
gateway_stop cancela el trabajo del host en curso, espera a que el trabajador termine y, a continuación,
cierra el adaptador.
Próximas obsolescencias
Algunas superficies adyacentes a los hooks están obsoletas, pero siguen siendo compatibles. Se deben migrar antes de la próxima versión principal:
- Sobres de canal de texto sin formato en los controladores
inbound_claimymessage_received. LeaBodyForAgenty los bloques estructurados de contexto del usuario en lugar de analizar el texto plano del sobre. Consulte Sobres de canal de texto sin formato → BodyForAgent. subagent_spawningse mantiene por compatibilidad con plugins anteriores, pero los plugins nuevos no deben devolver desde allí el enrutamiento de hilos. El núcleo prepara los enlaces de subagentesthread: truemediante adaptadores de enlace de sesiones de canal antes de que se activesubagent_spawned.deactivatese mantiene como alias obsoleto de compatibilidad para la limpieza hasta después de 2026-08-16. Los plugins nuevos deben usargateway_stop.onResolutionenbefore_tool_callahora utiliza la unión tipadaPluginApprovalResolution(allow-once/allow-always/deny/timeout/cancelled) en lugar de unstringde formato libre.api.registerSessionExtension/api.enqueueNextTurnInjectionse mantienen como alias de compatibilidad de nivel superior. Los plugins nuevos deben usarapi.session.state.registerSessionExtension(...)yapi.session.workflow.enqueueNextTurnInjection(...).
Para consultar la lista completa —registro de capacidades de memoria, perfil de razonamiento
del proveedor, proveedores de autenticación externos, tipos de descubrimiento de proveedores, accesores del entorno de ejecución
de tareas y el cambio de nombre de command-auth → command-status—, consulte
Migración del SDK de plugins → Obsolescencias activas.
Contenido relacionado
- Migración del SDK de plugins - obsolescencias activas y calendario de eliminación
- Creación de plugins
- Descripción general del SDK de plugins
- Puntos de entrada de plugins
- Hooks internos
- Detalles internos de la arquitectura de plugins