docs: add plugin hooks reference

This commit is contained in:
Peter Steinberger
2026-04-24 18:22:28 +01:00
parent 342583348d
commit 7330a0c7e0
10 changed files with 218 additions and 11 deletions

View File

@@ -33,7 +33,7 @@ falls back to npm automatically.
<Card title="Provider plugin" icon="cpu" href="/plugins/sdk-provider-plugins">
Add a model provider (LLM, proxy, or custom endpoint)
</Card>
<Card title="Tool / hook plugin" icon="wrench">
<Card title="Tool / hook plugin" icon="wrench" href="/plugins/hooks">
Register agent tools, event hooks, or services — continue below
</Card>
</CardGroup>
@@ -163,7 +163,8 @@ A single plugin can register any number of capabilities via the `api` object:
| Embedded Pi extension | `api.registerEmbeddedExtensionFactory(...)` | [SDK Overview](/plugins/sdk-overview#registration-api) |
| Agent tools | `api.registerTool(...)` | Below |
| Custom commands | `api.registerCommand(...)` | [Entry Points](/plugins/sdk-entrypoints) |
| Event hooks | `api.registerHook(...)` | [Entry Points](/plugins/sdk-entrypoints) |
| Plugin hooks | `api.on(...)` | [Plugin hooks](/plugins/hooks) |
| Internal event hooks | `api.registerHook(...)` | [Entry Points](/plugins/sdk-entrypoints) |
| HTTP routes | `api.registerHttpRoute(...)` | [Internals](/plugins/architecture-internals#gateway-http-routes) |
| CLI subcommands | `api.registerCli(...)` | [Entry Points](/plugins/sdk-entrypoints) |
@@ -197,7 +198,7 @@ If custom approval plumbing needs to detect that same bounded fallback case,
prefer `isApprovalNotFoundError` from `openclaw/plugin-sdk/error-runtime`
instead of matching approval-expiry strings manually.
See [SDK Overview hook decision semantics](/plugins/sdk-overview#hook-decision-semantics) for details.
See [Plugin hooks](/plugins/hooks) for examples and the hook reference.
## Registering agent tools

188
docs/plugins/hooks.md Normal file
View File

@@ -0,0 +1,188 @@
---
summary: "Plugin hooks: intercept agent, tool, message, session, and Gateway lifecycle events"
title: "Plugin hooks"
read_when:
- You are building a plugin that needs before_tool_call, before_agent_reply, message hooks, or lifecycle hooks
- You need to block, rewrite, or require approval for tool calls from a plugin
- You are deciding between internal hooks and plugin hooks
---
Plugin hooks are in-process extension points for OpenClaw plugins. Use them
when a plugin needs to inspect or change agent runs, tool calls, message flow,
session lifecycle, subagent routing, installs, or Gateway startup.
Use [internal hooks](/automation/hooks) instead when you want a small
operator-installed `HOOK.md` script for command and Gateway events such as
`/new`, `/reset`, `/stop`, `agent:bootstrap`, or `gateway:startup`.
## Quick start
Register typed plugin hooks with `api.on(...)` from your plugin entry:
```typescript
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
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,
timeoutBehavior: "deny",
},
};
},
{ priority: 50 },
);
},
});
```
Hook handlers run sequentially in descending `priority`. Same-priority hooks
keep registration order.
## Common hooks
| Hook | Use it for |
| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `before_tool_call` | Rewrite tool params, block execution, or request user approval before a tool runs. |
| `after_tool_call` | Observe tool results, errors, and duration after execution. |
| `before_prompt_build` | Add dynamic context or system prompt text before the model call. |
| `before_model_resolve` | Override provider or model before session messages are loaded. |
| `before_agent_reply` | Short-circuit the model turn with a synthetic reply or silence. |
| `llm_input` / `llm_output` | Observe provider input/output for conversation-aware plugins. |
| `agent_end` | Observe final messages, success state, and run duration. |
| `message_received` | Observe inbound channel messages after channel parsing. |
| `message_sending` | Rewrite or cancel outbound channel messages. |
| `message_sent` | Observe outbound delivery success or failure. |
| `session_start` / `session_end` | Track session lifecycle boundaries. |
| `before_compaction` / `after_compaction` | Observe or annotate compaction cycles. |
| `subagent_spawning` / `subagent_delivery_target` / `subagent_spawned` / `subagent_ended` | Coordinate subagent routing and completion delivery. |
| `gateway_start` / `gateway_stop` | Start or stop plugin services with the Gateway. |
| `before_install` | Inspect skill or plugin install scans and optionally block. |
## Tool call policy
`before_tool_call` receives:
- `event.toolName`
- `event.params`
- optional `event.runId`
- optional `event.toolCallId`
- context fields such as `ctx.agentId`, `ctx.sessionKey`, `ctx.sessionId`, and
diagnostic `ctx.trace`
It can return:
```typescript
type BeforeToolCallResult = {
params?: Record<string, unknown>;
block?: boolean;
blockReason?: string;
requireApproval?: {
title: string;
description: string;
severity?: "info" | "warning" | "critical";
timeoutMs?: number;
timeoutBehavior?: "allow" | "deny";
onResolution?: (decision: string) => Promise<void> | void;
};
};
```
Rules:
- `block: true` is terminal and skips lower-priority handlers.
- `block: false` is treated as no decision.
- `params` rewrites the tool parameters for execution.
- `requireApproval` pauses the agent run and asks the user through plugin
approvals. The `/approve` command can approve both exec and plugin approvals.
- A lower-priority `block: true` can still block after a higher-priority hook
requested approval.
## Prompt and model hooks
Use the phase-specific hooks for new plugins:
- `before_model_resolve`: receives only the current prompt and attachment
metadata. Return `providerOverride` or `modelOverride`.
- `before_prompt_build`: receives the current prompt and session messages.
Return `prependContext`, `systemPrompt`, `prependSystemContext`, or
`appendSystemContext`.
`before_agent_start` remains for compatibility. Prefer the explicit hooks above
so your plugin does not depend on a legacy combined phase.
Non-bundled plugins that need `llm_input`, `llm_output`, or `agent_end` must set:
```json
{
"plugins": {
"entries": {
"my-plugin": {
"hooks": {
"allowConversationAccess": true
}
}
}
}
}
```
Prompt-mutating hooks can be disabled per plugin with
`plugins.entries.<id>.hooks.allowPromptInjection=false`.
## Message hooks
Use message hooks for channel-level routing and delivery policy:
- `message_received`: observe inbound content, sender, `threadId`, and metadata.
- `message_sending`: rewrite `content` or return `{ cancel: true }`.
- `message_sent`: observe final success or failure.
Prefer typed `threadId` and `replyToId` fields before using channel-specific
metadata.
Decision rules:
- `message_sending` with `cancel: true` is terminal.
- `message_sending` with `cancel: false` is treated as no decision.
- Rewritten `content` continues to lower-priority hooks unless a later hook
cancels delivery.
## Install hooks
`before_install` runs after the built-in scan for skill and plugin installs.
Return additional findings or `{ block: true, blockReason }` to stop the
install.
`block: true` is terminal. `block: false` is treated as no decision.
## Gateway lifecycle
Use `gateway_start` for plugin services that need Gateway-owned state. The
context exposes `ctx.config`, `ctx.workspaceDir`, and `ctx.getCron?.()` for
cron inspection and updates. Use `gateway_stop` to clean up long-running
resources.
Do not rely on the internal `gateway:startup` hook for plugin-owned runtime
services.
## Related
- [Building plugins](/plugins/building-plugins)
- [Plugin SDK overview](/plugins/sdk-overview)
- [Plugin entry points](/plugins/sdk-entrypoints)
- [Internal hooks](/automation/hooks)
- [Plugin architecture internals](/plugins/architecture-internals)

View File

@@ -17,6 +17,7 @@ reference for **what to import** and **what you can register**.
- First plugin? Start with [Building plugins](/plugins/building-plugins).
- Channel plugin? See [Channel plugins](/plugins/sdk-channel-plugins).
- Provider plugin? See [Provider plugins](/plugins/sdk-provider-plugins).
- Tool or lifecycle hook plugin? See [Plugin hooks](/plugins/hooks).
</Tip>
## Import convention
@@ -229,6 +230,9 @@ AI CLI backend such as `codex-cli`.
| `api.on(hookName, handler, opts?)` | Typed lifecycle hook |
| `api.onConversationBindingResolved(handler)` | Conversation binding callback |
See [Plugin hooks](/plugins/hooks) for examples, common hook names, and guard
semantics.
### Hook decision semantics
- `before_tool_call`: returning `{ block: true }` is terminal. Once any handler sets it, lower-priority handlers are skipped.