Building plugins
Plugin hooks
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:
| You want to… | Use |
|---|---|
| Change prompts, gate tools, customize replies, or integrate plugin lifecycle | Typed plugin hooks on this page: api.on("before_tool_call", ...) |
Run an operator-installed script for /new, /reset, /stop, or bootstrap events |
Internal hooks: HOOK.md and colon event names such as command:new or agent:bootstrap |
| Trigger an agent from an external service over HTTP | Webhooks: Gateway HTTP endpoints |
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:
{ "name": "hook-demo", "version": "1.0.0", "type": "module", "openclaw": { "extensions": ["./index.ts"] }}{ "id": "hook-demo", "name": "Hook Demo", "activation": { "onStartup": true }, "configSchema": { "type": "object", "additionalProperties": false }}import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; export default definePluginEntry({ id: "hook-demo", name: "Hook Demo", description: "Reply to a hook check without a model call.", register(api) { api.on( "before_agent_reply", (event) => { if (event.cleanedBody.includes("hook-demo-check")) { return { handled: true, reply: { text: "Hook is working." } }; } }, { eligibleTriggers: ["user"] }, ); },});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):
openclaw plugins install --link ./hook-demo --forceopenclaw plugins enable hook-demoGrant this plugin access to conversation hooks in openclaw.json:
{ "plugins": { "entries": { "hook-demo": { "enabled": true, "hooks": { "allowConversationAccess": true } } } }}Merge that entry into your existing config, then let the default hybrid reload mode apply it and inspect:
openclaw plugins inspect hook-demo --runtime --jsonSend 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. Restart the Gateway 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: trueforbefore_model_resolve,agent_turn_prepare,before_prompt_build,before_agent_reply,llm_input,llm_output,before_agent_finalize,agent_end, andbefore_agent_run. Bundled plugins are allowed unless this option is explicitlyfalse. allowPromptInjection: falseblocksagent_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.
Choose a hook
| Task | Hook |
|---|---|
| Reply without a model call | before_agent_reply → { handled: true, reply }; omit reply for silence |
| Add context or narrow tools for a turn | before_prompt_build |
| Gate model input on a supported runner | before_agent_run → { outcome: "block", reason, message? } |
| Block a tool or request approval | before_tool_call |
| Rewrite the full outgoing reply, including media | reply_payload_sending |
| Rewrite outgoing text or cancel a send | message_sending |
| Collect model timing without raw conversation text | model_call_started / model_call_ended |
| Flush state after a turn or at shutdown | agent_end / gateway_stop |
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
| Symptom | Check |
|---|---|
| Plugin loads but the handler never runs | Use api.on for typed names, inspect openclaw plugins inspect <id> --runtime --json, and check diagnostics for blocked registrations. Runtime inspection loads the plugin in the inspecting process; restart the Gateway too. |
| Conversation hook is blocked | Set plugins.entries.<id>.hooks.allowConversationAccess: true; for prompt hooks, also check that allowPromptInjection is not false. These keys belong under hooks, not the plugin's config. |
| Hook works for one runtime or trigger only | Check the runtime boundary and eligibleTriggers. Missing context fields are not proof of a different sender, agent, or authorization state. |
| Persistence rewrite has no effect | Return { message } synchronously. An async handler's result is ignored. |
| A timed-out hook still performs work | Timeout ends the host's await, not plugin work. Pass available abort signals through I/O and bound plugin-owned work yourself. |
| One plugin's rewrite disappears | Check the hook's merge rule and priority. message_sending uses the last returned content; reply_payload_sending passes each updated payload onward. |
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_claimandmessage_receivedhandlers. Prefer typed fields instead of parsing flat envelope text:inbound_claimexposesevent.bodyForAgent;message_receivedexposesevent.contentand structured metadata, not aBodyForAgentfield. See Plaintext channel envelopes → BodyForAgent. onResolutioninbefore_tool_callnow uses the typedPluginApprovalResolutionunion (allow-once/allow-always/deny/timeout/cancelled) instead of a free-formstring.api.registerSessionExtension/api.enqueueNextTurnInjectionremain as top-level compatibility aliases. New plugins should useapi.session.state.registerSessionExtension(...)andapi.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.
- Debug runtime hooks
- Prompt and model hooks
- Authorized prompt enrichment
- Session extensions and next-turn injections
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.
Related
- Plugin SDK migration - active deprecations and removal timeline
- Building plugins
- Plugin SDK overview
- Plugin entry points
- Internal hooks
- Webhooks
- Plugin architecture internals