Skip to main content

Hooks

WednesdayAI has two distinct hook systems. They share the word “hook” but are registered, discovered, and fired differently. Pick the one that matches your use case.
Lifecycle hooksStandalone hooks
WhereInside a pluginA HOOK.md + handler directory
Registered viaapi.on(name, handler)Auto-discovery of HOOK.md
Fires on33 typed runtime eventsCoarse gateway/command events
Typical useTransform LLM input/output, observe model calls, context lifecycleRun code on /new, /reset, gateway start
Can mutate the run?Yes (some hooks)No — side-effect automation
There is no exported HookContext type. Lifecycle hook handlers receive a typed event and a typed ctx per hook name (see PluginHookHandlerMap). Standalone HOOK.md handlers receive their own runtime context. Do not import { HookContext } from the SDK — it does not exist.

Lifecycle hooks (in-plugin, via api.on)

Inside a plugin’s register function, attach a handler to a named lifecycle event:
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";

export default function register(api: OpenClawPluginApi): void {
  api.on("transform_llm_input", (event) => {
    // event.messages, event.provider, event.model are typed.
    // Return a modified messages array to mutate the run.
    return { messages: event.messages };
  });

  api.on("model_call_ended", (event) => {
    // Observe-only: read usage, headers, timing.
    api.logger.info(`model call ${event.outcome} in ${event.durationMs}ms`);
  });
}
api.on(name, handler, opts?) is fully typed: the event and ctx arguments are inferred from the hook name, and the allowed return type is enforced per hook. An optional { priority } controls ordering when multiple plugins hook the same event.

Mutation semantics

Lifecycle hooks fall into three categories:
  • Mutating — the handler’s return value replaces or amends the run. Examples: transform_llm_input / transform_llm_output (rewrite messages/output), before_tool_call (rewrite params or block), before_message_write (rewrite or block the message), before_prompt_build (append system prompt / context), before_model_resolve (override model/provider), before_context_send (return { messages? } to replace the outgoing message list), message_sending (rewrite or cancel), and the context.* / compaction.plan lifecycle hooks (contribute context, prune, plan compaction).
  • Observe-only — the return value is ignored. Examples: llm_input, llm_output, model_call_ended (read usage/headers), agent_end, session_start / session_end, gateway_start / gateway_stop.
  • Short-circuitingbefore_tool_call may return { block: true, blockReason } to stop a tool call; before_message_write may return { block: true } to drop a message.
model_call_ended is observe-only for run control but may mutate usage and proxyMetadata in place — core reads proxyMetadata back afterwards to record the actual provider/model. It fires once per run attempt (after the full tool-use loop), not once per individual LLM API call, and runs serially because of these in-place mutation semantics.

Lifecycle hook catalogue

These are the PluginHookName values accepted by api.on(...). Fire timing and whether the return value is honoured are noted.
HookFiresReturn honoured
before_model_resolveBefore the model/provider is chosen for a runYes — override modelOverride / providerOverride
before_prompt_buildBefore the system prompt and context are assembledYes — systemPrompt (appended after built sections), prependContext (inserted before the user turn)
before_context_sendJust before messages are sent to the modelYes — replace messages
before_agent_startLegacy combined pre-run hookYes — combines prompt-build and model-resolve results
llm_inputAfter input is prepared for the modelNo (observe)
llm_outputAfter the model returnsNo (observe)
model_call_endedOnce per run attempt, after the tool-use loopObserve; may mutate usage / proxyMetadata in place (serial)
agent_endWhen an agent run finishesNo (observe)
before_compactionBefore context compaction runsNo (observe)
after_compactionAfter compaction completesNo (observe)
before_resetOn /new or /reset, before the session clearsNo (observe)
message_receivedAn inbound message is receivedNo (observe)
message_sendingBefore an outbound message is sentYes — rewrite content or cancel
message_sentAfter an outbound message is sentNo (observe)
before_tool_callBefore a tool executesYes — rewrite params, or block with blockReason
after_tool_callAfter a tool executesNo (observe)
tool_result_persistBefore a tool result is written to the transcriptYes — replace the message
before_message_writeBefore any message is written to the JSONL transcriptYes — block or replace the message
session_startA session startsNo (observe)
session_endA session endsNo (observe)
subagent_spawningA subagent is about to spawnYes — { status: "ok"; threadBindingReady?: boolean } to allow, or { status: "error"; error: string } to block
subagent_delivery_targetResolving where a subagent reply is deliveredYes — override origin
subagent_spawnedA subagent has spawnedNo (observe)
subagent_endedA subagent endedNo (observe)
gateway_startThe gateway process startsNo (observe)
gateway_stopThe gateway process stopsNo (observe)
transform_llm_inputMutate the message list before the model callYes — return messages
transform_llm_outputMutate assistant output after the model callYes — return assistantTexts
storage.afterAppendAfter a conversation entry is appended to storageNo (observe)
context.collectCollecting context for the current turnYes — return a PromptContribution
context.projectProjecting collected contributions into messagesYes — messages, projection, contribution
context.prunePruning context under token pressureYes — messages, dropped, compactionHandled, contribution?
compaction.planPlanning a compaction passYes — plan, messages, contribution

before_prompt_build example

api.on("before_prompt_build", (event, ctx) => {
  // event.systemPrompt — the assembled system prompt (read-only; do not mutate)
  // event.messages — session messages prepared for this run

  return {
    // Appended after the built system prompt sections:
    systemPrompt: "Always respond in the user's language.",

    // Inserted before the user turn in the message list:
    prependContext: `Current user timezone: ${getUserTimezone(ctx.sessionKey)}`,
  };
});
Both systemPrompt and prependContext are optional — return either, both, or neither. systemPrompt is appended once per run after the full section build; prependContext is a plaintext string inserted into the context immediately before the user’s message.
The context.* and compaction.plan hooks are the context-engine surface. A plugin that fully owns compaction can set ownsCompaction: true in its plugin definition (or return compactionHandled: true from context.prune) so the runtime skips its built-in compaction trigger and avoids double-compaction races.

Standalone hooks (HOOK.md)

Standalone hooks are directories auto-discovered by the gateway. They run side-effect automation on coarse events and do not modify the run.
my-hook/
├── HOOK.md          # metadata + docs (frontmatter drives discovery)
└── handler.ts       # handler module
HOOK.md frontmatter declares the events the hook handles:
---
name: my-hook
description: "Saves a marker when the session resets"
metadata: { "openclaw": { "emoji": "💾", "events": ["command:new", "command:reset"] } }
---
Common standalone events include command:new, command:reset, command:stop, agent:bootstrap, gateway:startup, message:received, message:transcribed, message:preprocessed, and message:sent. The handler exports a function (default export, or the name given by metadata.openclaw.export) that runs when a declared event fires. message:transcribed fires when an inbound audio message has been transcribed. message:preprocessed fires after the message has been enriched with links, images, and transcripts — immediately before routing. Both are useful for logging pipelines and external integrations that need the enriched message body without modifying the run.

Bundled standalone hooks

WednesdayAI ships several bundled hooks, disabled by default. Enable them with the CLI:
openclaw hooks list             # see all discovered hooks + status
openclaw hooks enable session-memory
openclaw hooks info session-memory
openclaw hooks check
session-memory and session-journal are both session-reset hooks — session-memory writes a structured summary of the conversation, while session-journal makes an LLM call to produce a reflective journal entry. session-journal is enabled per-agent via sessionJournal.enabled: true in the agent config rather than via hooks.internal.entries. See the Hooks catalogue for the full list and configuration details.

Discovery and precedence

Standalone hooks are discovered from (highest precedence first):
  1. <workspace>/hooks/ — per-agent hooks
  2. ~/.openclaw/hooks/ — user-installed shared hooks
  3. bundled hooks shipped with WednesdayAI

Installing hook packs

openclaw hooks install @wednesdayai/my-hook-pack   # npm pack
openclaw hooks install ./path/to/my-hook           # local directory
openclaw hooks install runs npm install --ignore-scripts. Keep dependencies to pure JS/TS with no postinstall build steps.

Which hook should I use?

  • Need to transform the run (rewrite LLM input/output, gate tool calls, shape context, observe model usage)? Use a lifecycle hook in a plugin via api.on(...).
  • Need to run a side effect on a coarse event (/new, /reset, gateway start) without modifying the run? Use a standalone HOOK.md hook.

What’s next