Scott Glover's commit 371b69b3e2 ('Expose cron jobId in plugin hook
context') added an optional jobId field on PluginHookAgentContext,
populated for cron-driven runs. The commit shipped without a docs
update or CHANGELOG entry, so plugin authors had no visible signal
that the new ctx.jobId field exists.
Surface ctx.jobId in two existing hook context references in
docs/plugins/hooks.md: the before_tool_call ctx-fields list, and the
runId/agent-lifecycle paragraph that already names ctx.runId — extend
it to note ctx.jobId on cron-driven runs and what plugins can do with
it (scope metrics, side effects, or state to a scheduled job).
11 KiB
summary, title, read_when
| summary | title | read_when | |||
|---|---|---|---|---|---|
| Plugin hooks: intercept agent, tool, message, session, and Gateway lifecycle events | 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 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:
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.
Hook catalog
Hooks are grouped by the surface they extend. Names in bold accept a decision result (block, cancel, override, or require approval); all others are observation-only.
Agent turn
before_model_resolve— override provider or model before session messages loadbefore_prompt_build— add dynamic context or system-prompt text before the model callbefore_agent_start— compatibility-only combined phase; prefer the two hooks abovebefore_agent_reply— short-circuit the model turn with a synthetic reply or silencebefore_agent_finalize— inspect the natural final answer and request one more model passagent_end— observe final messages, success state, and run duration
Conversation observation
model_call_started/model_call_ended— observe sanitized provider/model call metadata, timing, outcome, and bounded request-id hashes without prompt or response contentllm_input— observe provider input (system prompt, prompt, history)llm_output— observe provider output
Tools
before_tool_call— rewrite tool params, block execution, or require approvalafter_tool_call— observe tool results, errors, and durationtool_result_persist— rewrite the assistant message produced from a tool resultbefore_message_write— inspect or block an in-progress message write (rare)
Messages and delivery
inbound_claim— claim an inbound message before agent routing (synthetic replies)message_received— observe inbound content, sender, thread, and metadatamessage_sending— rewrite outbound content or cancel deliverymessage_sent— observe outbound delivery success or failurebefore_dispatch— inspect or rewrite an outbound dispatch before channel handoffreply_dispatch— participate in the final reply-dispatch pipeline
Sessions and compaction
session_start/session_end— track session lifecycle boundariesbefore_compaction/after_compaction— observe or annotate compaction cyclesbefore_reset— observe session-reset events (/reset, programmatic resets)
Subagents
subagent_spawning/subagent_delivery_target/subagent_spawned/subagent_ended— coordinate subagent routing and completion delivery
Lifecycle
gateway_start/gateway_stop— start or stop plugin-owned services with the Gatewaybefore_install— inspect skill or plugin install scans and optionally block
Tool call policy
before_tool_call receives:
event.toolNameevent.params- optional
event.runId - optional
event.toolCallId - context fields such as
ctx.agentId,ctx.sessionKey,ctx.sessionId,ctx.runId,ctx.jobId(set on cron-driven runs), and diagnosticctx.trace
It can return:
type BeforeToolCallResult = {
params?: Record<string, unknown>;
block?: boolean;
blockReason?: string;
requireApproval?: {
title: string;
description: string;
severity?: "info" | "warning" | "critical";
timeoutMs?: number;
timeoutBehavior?: "allow" | "deny";
pluginId?: string;
onResolution?: (
decision: "allow-once" | "allow-always" | "deny" | "timeout" | "cancelled",
) => Promise<void> | void;
};
};
Rules:
block: trueis terminal and skips lower-priority handlers.block: falseis treated as no decision.paramsrewrites the tool parameters for execution.requireApprovalpauses the agent run and asks the user through plugin approvals. The/approvecommand can approve both exec and plugin approvals.- A lower-priority
block: truecan still block after a higher-priority hook requested approval. onResolutionreceives the resolved approval decision —allow-once,allow-always,deny,timeout, orcancelled.
Tool result persistence
Tool results can include structured details for UI rendering, diagnostics,
media routing, or plugin-owned metadata. Treat details as runtime metadata,
not prompt content:
- OpenClaw strips
toolResult.detailsbefore provider replay and compaction input so metadata does not become model context. - Persisted session entries keep only bounded
details. Oversized details are replaced with a compact summary andpersistedDetailsTruncated: true. tool_result_persistandbefore_message_writerun before the final persistence cap. Hooks should still keep returneddetailssmall and avoid placing prompt-relevant text only indetails; put model-visible tool output incontent.
Prompt and model hooks
Use the phase-specific hooks for new plugins:
before_model_resolve: receives only the current prompt and attachment metadata. ReturnproviderOverrideormodelOverride.before_prompt_build: receives the current prompt and session messages. ReturnprependContext,systemPrompt,prependSystemContext, orappendSystemContext.
before_agent_start remains for compatibility. Prefer the explicit hooks above
so your plugin does not depend on a legacy combined phase.
before_agent_start and agent_end include event.runId when OpenClaw can
identify the active run. The same value is also available on ctx.runId.
Cron-driven runs also expose ctx.jobId (the originating cron job id) so
plugin hooks can scope metrics, side effects, or state to a specific scheduled
job.
Use model_call_started and model_call_ended for provider-call telemetry
that should not receive raw prompts, history, responses, headers, request
bodies, or provider request IDs. These hooks include stable metadata such as
runId, callId, provider, model, optional api/transport, terminal
durationMs/outcome, and upstreamRequestIdHash when OpenClaw can derive a
bounded provider request-id hash.
before_agent_finalize runs only when a harness is about to accept a natural
final assistant answer. It is not the /stop cancellation path and does not
run when the user aborts a turn. Return { action: "revise", reason } to ask
the harness for one more model pass before finalization, { action: "finalize", reason? } to force finalization, or omit a result to continue.
Codex native Stop hooks are relayed into this hook as OpenClaw
before_agent_finalize decisions.
Non-bundled plugins that need llm_input, llm_output,
before_agent_finalize, or agent_end must set:
{
"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,messageId,senderId, optional run/session correlation, and metadata.message_sending: rewritecontentor return{ cancel: true }.message_sent: observe final success or failure.
For audio-only TTS replies, content may contain the hidden spoken transcript
even when the channel payload has no visible text/caption. Rewriting that
content updates the hook-visible transcript only; it is not rendered as a
media caption.
Message hook contexts expose stable correlation fields when available:
ctx.sessionKey, ctx.runId, ctx.messageId, ctx.senderId, ctx.trace,
ctx.traceId, ctx.spanId, ctx.parentSpanId, and ctx.callDepth. Prefer
these first-class fields before reading legacy metadata.
Prefer typed threadId and replyToId fields before using channel-specific
metadata.
Decision rules:
message_sendingwithcancel: trueis terminal.message_sendingwithcancel: falseis treated as no decision.- Rewritten
contentcontinues 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.
Upcoming deprecations
A few hook-adjacent surfaces are deprecated but still supported. Migrate before the next major release:
- Plaintext channel envelopes in
inbound_claimandmessage_receivedhandlers. ReadBodyForAgentand the structured user-context blocks instead of parsing flat envelope text. See Plaintext channel envelopes → BodyForAgent. before_agent_startremains for compatibility. New plugins should usebefore_model_resolveandbefore_prompt_buildinstead of the combined phase.onResolutioninbefore_tool_callnow uses the typedPluginApprovalResolutionunion (allow-once/allow-always/deny/timeout/cancelled) instead of a free-formstring.
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.
Related
- Plugin SDK migration — active deprecations and removal timeline
- Building plugins
- Plugin SDK overview
- Plugin entry points
- Internal hooks
- Plugin architecture internals