Skip to main content
Registration rules, execution contracts, per-handler budgets, and the complete typed hook catalog. Part of the Plugin hooks guide.

Registration and execution

Keep register(api) synchronous and register handlers there. The handlers themselves may be asynchronous except for the two synchronous persistence hooks. Handlers default to priority 0; higher priorities run first, with registration order breaking ties. Execution depends on the hook kind: Priority does not serialize observation side effects. Fire-and-forget events can overlap later events, and callbacks are not a durable event queue. Return modifications explicitly instead of relying on in-place mutation. api.on(name, handler, opts?) accepts: Trigger eligibility is enforced by the host before it invokes the handler. A hook registered with eligibleTriggers: ["heartbeat", "cron"] is therefore inactive for user turns, including a recovered user turn. Omitted, empty, malformed, or partly unknown lists remain unrestricted, so the hook runs for those turns. Other hook kinds do not accept this option. Operators can set hook budgets without patching plugin code:
hooks.timeouts.<hookName> overrides hooks.timeoutMs, which overrides the plugin-authored api.on(..., { timeoutMs }) value. The two operator config fields accept positive integers up to 600000 ms. Prefer per-hook overrides for known-slow hooks so one plugin does not get a longer budget everywhere. A timed-out handler promise continues running because hook callbacks do not receive a timeout-owned cancellation signal. before_tool_call may receive the owning tool call’s ctx.abortSignal, but hook timeout expiry does not abort it. The hook dispatch can release its Gateway admission while that plugin work is still in progress. Plugins that own long-running work must provide their own cancellation and shutdown lifecycle. The standard runner applies these defaults per handler: An emitter can impose a tighter overall lifecycle budget, such as the shutdown session_end drain below. A timeout only bounds an asynchronous await; it cannot interrupt synchronous JavaScript. For a policy requirement, use a fail-closed gate rather than assuming an observation or delivery hook will reject the operation on failure. For claim hooks, continuing means trying the next handler. The caller decides what happens if nobody claims; a failed inbound_claim for a bound conversation can produce a binding notice instead of an ordinary agent reply. Channel plugins that use createReplyDispatcher can likewise declare a larger positive per-stage budget with beforeDeliverOptions: { timeoutMs }, or when appending work with dispatcher.appendBeforeDeliver(handler, { timeoutMs }). Without an owner-declared budget, those callbacks use the same 15-second default so a hung callback cannot retain the serialized delivery lane.

Hook catalog

Hooks are grouped by the surface they extend. Kinds refer to the execution contracts above; a modifying hook is not an observation hook. Agent turn Conversation observation Tools Messages and delivery inbound_claim is not a global pre-routing broadcast. OpenClaw invokes it only for the plugin that owns the message’s core-managed conversation binding. To suppress an ordinary agent turn before model input without retaining the original prompt in transcript, use before_agent_run on a supported runner. To short-circuit an agent turn with a synthetic reply or silence, use before_agent_reply. Sessions and compaction Successful engine-owned compaction attempts emit after_compaction even when no history changes, with compactedCount: 0. Failed or aborted attempts do not emit that completion hook. session_end.reason is one of new, reset, idle, daily, compaction, deleted, shutdown, restart, or unknown. session_start has no reason field; it can include resumedFrom. Shutdown/restart events come from the Gateway finalizer for active sessions, so plugins can close session state before the process exits. Shutdown and restart share one 2-second total session_end drain budget across all active sessions and plugin handlers; the budget is not per handler. Return quickly or keep finalization bounded and persistence crash-consistent. If the budget expires, OpenClaw logs shutdown session-end drain timed out and continues shutdown, so unfinished plugin work can be interrupted. For sessions.create calls with parentSessionKey and emitCommandHooks: true, a distinct child always receives session_start. Callers declare whether the parent also receives terminal session_end with succeedsParent: true means successor, false means parallel child. Omission preserves the legacy parent-rollover behavior. The command:new and before_reset hooks still describe the requested /new action in both cases. Subagents
  • subagent_spawned / subagent_ended - observe subagent launch and completion.
  • subagent_progress - observe portable started / ended progress for a background child run; includes runId, childSessionKey, optional requester route, and an outcome on ended.
  • subagent_delivery_target - modifying compatibility hook for completion delivery when no core session binding can project a route. The first returned origin wins.
  • subagent_spawned includes resolvedModel and resolvedProvider when OpenClaw has resolved the child session’s native model before launch.
  • subagent_ended carries targetSessionKey (identity - matches subagent_spawned.childSessionKey), targetKind ("subagent" or "acp"), reason, optional outcome ("ok", "error", "timeout", "killed", "reset", or "deleted"), optional error, runId, endedAt, accountId, and sendFarewell. It does not include agentId or childSessionKey; use targetSessionKey to correlate with the matching subagent_spawned event.
Lifecycle

Skill lifecycle and evaluation

Use skill_proposal_evaluate for static analyzers, security scanners, benchmarks, model-based graders, or other third-party evaluators. OpenClaw passes an immutable candidate bundle with file hashes and a tree hash. Update proposals also include the complete current skill as baseline. Text files use UTF-8 content; binary files use base64. Evaluator registrations run concurrently. Give each evaluator a stable registrationId:
When evaluation input includes correlationId, OpenClaw forwards it to the evaluator event for both manual and apply-triggered evaluations. This value is caller-supplied correlation metadata, not authenticated identity or proof of authorization. An authorization plugin must mint or replace the value through a trusted entry point, bind it to the intended operation, and validate and consume it itself. Stored outcomes identify the evaluator, plugin id, plugin package version, status, and returned result. Timeouts and thrown errors are recorded as attributed error outcomes; they do not fail the whole evaluation. Among evaluator outcomes, only a completed decision: "block" vetoes apply. Other Workshop validation and ownership checks still apply. Apply revalidates the evaluated target tree under the Workshop mutation lock, so any live skill asset drift requires reevaluation. The complete persisted evaluation envelope is capped at 512 KiB. skill_proposal_changed fires after the matching proposal row and append-only lifecycle event commit. It carries the event id, sequence, exact proposal revision hash, optional correlation id, and evaluation outcomes. skill_changed fires after a live skill create, update, or removal commits and includes optional before/after artifacts with content and tree hashes, plus declared and source versions when available. These hooks are primitives, not an optimization scheduler. A plugin or external controller can observe a durable proposal event, evaluate its exact revision hash, revise with that hash and a correlation id, then repeat. OpenClaw does not automatically revise proposals or run an unbounded evaluation loop. Event replay is byte-bounded and returns nextSequence when another page is available.

Channel pairing requests

Use channel_pairing_requested when a plugin needs to notify an operator or write an audit record after an unpaired DM sender creates a pending pairing request. The hook is dispatched when the request is created; channel delivery of the pairing reply is not delayed by slow or failing hook handlers.
The hook is observation-only. It does not approve, reject, suppress, or rewrite the pairing reply. The payload includes the channel, optional accountId, channel-scoped senderId, pairing code, and channel metadata. Treat the pairing code as a live single-use approval credential and deliver it only to a trusted operator sink. Treat metadata as untrusted sender-supplied identity text. The hook does not include the inbound message body or media.