Skip to main content
Plugin hooks let a native OpenClaw plugin observe or change agent runs, tool calls, message delivery, and lifecycle events. Register a typed handler with api.on("hook_name", handler) and return the result documented for that hook. There are three different hook systems: Plugins can also register internal hooks with api.registerHook(...). That is not the typed API: registering an underscore name such as before_tool_call there produces a warning, and the typed runner never invokes that registration. Use api.on(...) for every hook in the hook catalog.

Quick start

This example replies to a user message containing hook-demo-check without calling the model. It assumes you already have a working Gateway and can send it a normal chat message. For package metadata, publishing, and install options, see Building plugins and Plugin manifest. Create a local hook-demo directory with these files:
package.json
openclaw.plugin.json
index.ts
Review local plugin code before loading it: native plugins run in the Gateway process. Link and enable the directory (--force acknowledges installing from a local source):
Grant this plugin access to conversation hooks in openclaw.json:
Merge that entry into your existing config, then let the default hybrid reload mode apply it and inspect:
Send hook-demo-check as a normal chat message. Expect Hook is working.; other messages continue through the normal agent path. If the hook does not run, see Troubleshooting. Despite its name, cleanedBody is the prepared run prompt and can contain channel context. The example matches a distinctive marker instead of assuming the field is only the sender’s raw text.

Permissions and scope

Hook registration does not bypass plugin loading rules. The plugin must be loaded and enabled; plugins.enabled, plugins.allow, and plugins.deny still apply. Run openclaw plugins reload <id> after changing plugin code. With the default hybrid reload mode, hook policy changes hot-reload the existing plugin runtime.
  • Non-bundled plugins need explicit plugins.entries.<id>.hooks.allowConversationAccess: true for before_model_resolve, agent_turn_prepare, before_prompt_build, before_agent_reply, llm_input, llm_output, before_agent_finalize, agent_end, and before_agent_run. Bundled plugins are allowed unless this option is explicitly false.
  • allowPromptInjection: false blocks agent_turn_prepare, before_prompt_build, heartbeat_prompt_contribution, and durable next-turn injections. It defaults to allowed, but does not grant conversation access. The first two hooks therefore need both permissions.
  • These are specific registration gates, not a sandbox or a universal filter for every hook that can see message data. Install only plugins you trust.
A typed handler receives (event, ctx). The event describes the operation; the second argument carries hook-specific context. Fields such as ctx.agentId, ctx.sessionKey, and ctx.runId are optional on many hooks and may be absent for the emitting path. A registration is not automatically scoped to one agent or session: check the context in your handler when needed. Read your plugin’s resolved settings from api.pluginConfig inside the registration closure. Typed hooks do not receive a universal event.context.pluginConfig field; that field belongs to the internal api.registerHook(...) event contract. By default in hybrid reload mode, editing plugins.entries.<id>.config replaces the plugin instance and reruns registration with the new settings.

Choose a hook

The catalog is the registration API, not a promise that every runtime emits every hook. For example, before_agent_run is implemented by the embedded and CLI runners; do not rely on it as a Codex or Copilot input gate. Native tool, transcript, and compaction boundaries also differ. See Codex hook boundaries and Agent harness plugins.

Troubleshooting

Upcoming deprecations

A few hook-adjacent surfaces are deprecated but still supported. Removal eligibility is tracked per surface in the plugin compatibility registry, as a removeAfter date or an explicit removal gate, not at a major-version boundary. Migrate now:
  • Plaintext channel envelopes in inbound_claim and message_received handlers. Prefer typed fields instead of parsing flat envelope text: inbound_claim exposes event.bodyForAgent; message_received exposes event.content and structured metadata, not a BodyForAgent field. See Plaintext channel envelopes → BodyForAgent.
  • onResolution in before_tool_call now uses the typed PluginApprovalResolution union (allow-once / allow-always / deny / timeout / cancelled) instead of a free-form string.
  • api.registerSessionExtension / api.enqueueNextTurnInjection remain as top-level compatibility aliases. New plugins should use api.session.state.registerSessionExtension(...) and api.session.workflow.enqueueNextTurnInjection(...).
For the full list - memory capability registration, provider thinking profile, external auth providers, provider discovery types, task runtime accessors, and the command-auth → command-status rename - see Plugin SDK migration → Active deprecations.

Where each section moved

Every section of the single-page version now lives on this page or on one of the five child pages below. The anchors from the single-page version still resolve here.

Hook reference

Hook reference — Registration rules, execution contracts, per-handler budgets, and the complete typed hook catalog.

Tool call policy hooks

Tool call policy hooks — Parameter rewrites, blocks, approvals, exec environment contributions, and transcript persistence.

Prompt and session hooks

Prompt and session hooks — Model resolution, prompt construction, finalization, and durable plugin-owned session state.

Message and delivery hooks

Message and delivery hooks — Inbound interception, reply takeover, and outbound delivery policy.

Gateway and install lifecycle hooks

Gateway and install lifecycle hooks — Install-time checks, Gateway service lifecycle, and safe external cron projection.