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.
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:
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.

before_prompt_build example

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.

Runtime and turn identity context

Agent, message (message_*), tool, and before_message_write hook contexts carry two optional identity objects alongside their per-hook fields:
  • ctx.runtimeIdentity?: PluginRuntimeIdentity — who and where: agentId, sessionKey, channel ids, sender fields (senderId, senderName, senderE164, canonicalIdentity), and the privacy-safe digests providerConversationId / hashedUserId (truncated SHA-256, never raw provider ids). identitySource ("sender" | "parent-session" | "job-config") is the provenance of canonicalIdentity and is never defaulted — absent provenance means the emitting seam did not know it.
  • ctx.turn?: PluginTurnIdentity — correlation for the current turn: turnId (falls back to the run id), runId, sessionId, turnNumber, inboundMessageId, userEntryId / assistantEntryId, and per-session plus global sequence numbers.
Treat every field as optional: hooks fired outside a channel run (gateway start, cron) may carry neither object. See the plugin hooks reference for the full typed contract.

Inbound channel replies

message_sending and message_sent fire not only on adapter-initiated outbound sends but also on replies to inbound channel messages — every built-in channel (Telegram, Discord, Slack, Signal, iMessage, WhatsApp web, Matrix, MS Teams) routes inbound reply delivery through the same shared helpers (applyMessageSendingHook / createMessageSentEmitter, re-exported from openclaw/plugin-sdk). message_sending rewrites payload text only — media is never rewritten. A throwing handler is logged and the original payload still sends. message_sent fires only after a real send attempt, including after partial sends of chunked replies.

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.
HOOK.md frontmatter declares the events the hook handles:
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:
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 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