OpenClaw Hook Event Types: Pick the Right Runtime Contract

OpenClaw Hook Event Types: Pick the Right Runtime Contract

OpenClaw hook event types look like small strings: command:new, agent:bootstrap, message:sent. That makes them easy to treat as labels. They are not labels. Each one is a runtime contract that tells you who emits the event, whether the producer waits, which context fields are present, and whether any reply text will be delivered back to a user.

That distinction matters once hooks move out of demos. A hook that logs a reset command can be simple. A hook that saves session state, adds bootstrap files, watches message delivery, or reacts during shutdown needs sharper boundaries. Pick the wrong event and the handler may run too late, run twice, miss the data you expected, or quietly push a message that nobody will ever see.

The practical rule is plain: choose the event by the job’s timing and verification signal, not by the name that sounds closest.

What OpenClaw hook event types actually control

Internal hooks run inside the OpenClaw Gateway process. A hook declares the events it wants in HOOK.md under metadata.openclaw.events, then exports a handler. When the Gateway emits a matching event, OpenClaw calls the handler with an event object containing a family, action, session correlation key, timestamp, context object, and a messages array.

The event key controls four things that operators often blur together:

  • Producer: the code path that emits the event, such as command handling, compaction, bootstrap, Gateway lifecycle, or message delivery.
  • Wait behavior: whether the producer awaits the handler, dispatches it independently, or applies a bounded initial wait.
  • Context shape: the event-specific fields available under event.context.
  • Reply behavior: whether strings added to event.messages can be delivered anywhere.

This is why a hook can be “loaded” and still disappoint you. The handler may be subscribed to a real event, but the producer may ignore replies, omit the field you assumed existed, or emit only on a specific Gateway path.

The event families at a glance

OpenClaw groups internal events into five families: command, session, agent, gateway, and message. You can subscribe to an exact key, such as command:new, or to a bare family, such as command. A family listener receives every action in that family.

Here is the operator-friendly map.

  • Command events: command:new, command:reset, and command:stop. Use these when the side effect belongs to authorized command handling.
  • Session events: session:auto-reset, session:compact:before, session:compact:after, and session:patch. Use these when the work is about session lifecycle, compaction, or persisted session settings.
  • Agent events: agent:bootstrap. Use this when the hook needs to influence bootstrap context before it is injected.
  • Gateway events: gateway:startup, gateway:shutdown, and gateway:pre-restart. Use these for process lifecycle work, not per-message logic.
  • Message events: message:received, message:transcribed, message:preprocessed, and message:sent. Use these for observation and lightweight telemetry around accepted inbound or outbound message paths.

One trap: session:compact is not a family or wildcard in the documented contract. Subscribe to session:compact:before and session:compact:after explicitly.

Awaited, bounded, and asynchronous events

The first design question is whether the producer waits for your hook. If it does, your handler sits in the path of user-visible work. If it does not, you are observing something after admission or in parallel with other processing.

command:new and command:reset are awaited on supported command paths. So are session:compact:before, session:compact:after, and agent:bootstrap. These are good places for short, bounded work that must finish before the operation continues. They are bad places for slow network calls without timeouts.

session:auto-reset is dispatched independently of the successor turn. That makes sense: automatic rollover should not become a hidden blocker for the next conversation. If you save an excerpt or write a file there, verify the artifact after the background work finishes instead of assuming it exists immediately.

session:patch and the message events are asynchronous notifications or observations. They can overlap with other events and agent processing. Treat them as monitoring points, not lock points.

Gateway shutdown deserves special care. gateway:shutdown and gateway:pre-restart have bounded initial waits so teardown can proceed. The timeout does not cancel your handler, though. Before closing shared state, the Gateway joins actual hook completion. A handler that never settles can still prevent in-process shutdown from finishing. Shutdown hooks should close files, flush a small buffer, or notify a local service. They should not start an unbounded workflow.

Context is producer-shaped, not universal

The event object always has a context property, but that does not mean every context looks alike. The producer decides what it can safely provide.

command:new and command:reset can include fields such as agentId, sessionEntry, previousSessionEntry, commandSource, senderId, workspaceDir, storePath, and cfg on the chat command path. Gateway callers can omit senderId. A sessionFile value can be a transcript identifier rather than a readable JSONL file path. Code that assumes the chat path everywhere will break on legitimate Gateway paths.

command:stop is smaller. It can carry sessionEntry, sessionId, commandSource, and senderId, but it does not carry the full new or reset context. It also has no hook reply delivery. That makes it useful for cancellation logging, not for an “agent has finished” action. For finalization-style contracts, the plugin hook docs point you toward typed plugin hooks such as before_agent_finalize.

agent:bootstrap is the unusual one because it exposes a mutable bootstrapFiles array. A handler can replace or extend that array, but final path deduplication, privacy filtering, and context budgets still apply. That is a narrow mutation point, not permission to rewrite arbitrary Gateway state.

Compaction events include counts and optional token data. Optional means optional. Do not infer missing token counts as zero, and do not use a before event as proof that compaction succeeded. Compaction can skip or fail after the before event, and retries can emit before again.

Message events are observations, not a transport audit

The message family is tempting because it sounds broad. In practice, these events are observation points around accepted dispatch, transcription, preprocessing, and send outcomes. They are not a complete audit of every transport update, every duplicate, or every low-level send attempt.

message:received requires an accepted inbound dispatch with a session key. Suppressed or duplicate inbound updates can omit it. Paths with no session key can omit it too. message:transcribed fires when pre-agent preprocessing has nonempty audio transcript text and a session key. message:preprocessed means the preprocessing phase completed or was skipped; it does not promise every attachment or link was understood.

message:sent reports a delivery owner outcome with a session key. Its success field matters, but absence of an event is not proof of success or failure. Outbound delivery may report one outcome per logical payload rather than per text chunk. A partial failure can include a message ID for a part already sent, so blindly resending from a hook can duplicate a delivered message.

Use message events for small audit rows, metrics, or diagnostics. Do not use them as a durable delivery queue or as a policy gate. If you need to rewrite, block, or approve message behavior, use the typed plugin hook surface instead.

Family subscriptions can fire twice

OpenClaw lets you subscribe to a family such as command. That is useful for generic logging. It is also an easy way to create duplicate side effects.

If the same handler subscribes to both command and command:new, it can be called twice for a new command. Handlers for one event run sequentially: family listeners first, then exact listeners, in registration order within each group. The dispatcher catches and logs thrown errors and continues to later handlers.

That behavior is good for resilience, but it means your handler should be idempotent when it writes files, sends network requests, or stores state. A log append with a unique timestamp is usually fine. A payment, external ticket, or destructive cleanup needs stronger safeguards and probably does not belong in a simple internal hook.

Internal hooks vs plugin hooks

OpenClaw has more than one hook system, and the names are intentionally different. Internal hooks use colon event names such as command:new and live in hook directories with HOOK.md. Plugin hooks use a typed API, api.on("hook_name", handler), with names such as before_tool_call, before_agent_reply, and message_sending.

Use internal hooks when you want operator-installed Gateway scripts for command, session, bootstrap, Gateway lifecycle, or message observation events. Use plugin hooks when you need typed lifecycle control, prompt changes, tool gating, reply rewrites, approval flows, or plugin-owned services with a clearer shutdown lifecycle.

The docs make one boundary especially clear: registering typed names like before_tool_call through the legacy internal hook registration path does not invoke the typed runner. New integrations that need lifecycle control should use api.on(...).

How to choose the right OpenClaw hook event type

Start with the side effect, then choose the event. Here is a quick decision guide.

  • Log authorized new, reset, or stop commands: use the command family or exact command events. Keep the log minimal because session and sender identifiers may be private.
  • Save recent session context before a manual reset: use command:new and command:reset, or use the bundled session-memory hook if it fits.
  • Save context on daily or idle rollover: use session:auto-reset, and expect independent background dispatch.
  • Add workspace files to model context: use agent:bootstrap, and modify context.bootstrapFiles carefully.
  • Notify users around compaction: use session:compact:before and session:compact:after, but remember delivery depends on the caller’s notice callback.
  • Observe inbound or outbound messages: use message events for diagnostics only. Do not treat them as complete transport records.
  • Run startup work: use gateway:startup, understanding that it is scheduled after hook loading and sidecar or channel startup work and does not delay the initial Gateway bind.
  • Flush local state on shutdown: use gateway:shutdown or gateway:pre-restart, with bounded and settling work only.
  • Change prompts, block tools, or rewrite replies: use plugin hooks, not internal hook event types.

How to verify an event actually ran

Do not stop at openclaw hooks check or a “ready” report. Those checks are useful, but they do not prove that the running Gateway imported the handler, that the configured selection includes it, or that the specific event fired.

A solid verification loop has four steps.

  1. Inspect the hook with openclaw hooks info <hook-name> and confirm the declared event keys are the ones you meant.
  2. Confirm config selection: hooks.internal.enabled, named entries, workspace opt-in, and the hook’s hookKey.
  3. Trigger the exact producer path. A normal chat /new is not the same as every Gateway session create call. A manual compaction path may not deliver the same notices as another compaction caller.
  4. Verify a hook-specific marker: a log line, a JSONL append, a memory file, a changed bootstrap file list, or a delivery outcome record.

If a marker appears but the chat reply does not, debug reply delivery, not event execution. Only chat command handling for /new and /reset attempts to route event.messages as replies. Gateway session RPCs can emit command events without routing messages as chat replies. Most other producers ignore event.messages.

Operational guardrails

Internal hooks are trusted Gateway code. They are not sandboxed scripts. Review them like you would review any code running with filesystem, network, and environment access on the Gateway host.

Keep handlers short. Await the work that belongs to the handler, set timeouts on I/O, cap payload sizes, and filter unrelated events early. Avoid logging message bodies, whole config objects, or credentials. If the job needs a queue, retries, long-lived sockets, watchers, or a durable workflow, move that work to a plugin service, an automation, or an external system that owns the lifecycle.

The right hook is usually the narrow one. Exact event keys make behavior easier to test, easier to explain, and less likely to fire during a neighboring operation you did not mean to touch.

FAQ

Can I subscribe to every command event with one key?

Yes. Subscribe to the command family. Just do not also subscribe the same handler to command:new unless you want duplicate calls for new commands.

Does message:sent prove the user received and read the message?

No. It reports a send outcome from a delivery owner on a path that emitted the event. It is not a read receipt, and absence of the event is not proof of either success or failure.

Can an unknown event key work?

Only if custom code explicitly emits it. Unknown subscriptions are still registered, but the loader warns and core will not emit a typo such as command:nwe.

Should I use command:stop when an agent finishes?

No. command:stop observes cancellation command handling after the abort request. It is not an agent-finalization hook. Use the plugin lifecycle surface for finalization contracts.

Where should I add context files before a run?

Use agent:bootstrap if you are writing an internal hook. That event exposes mutable bootstrapFiles, and OpenClaw still applies its normal filtering, deduplication, and context limits afterward.

Bottom line

OpenClaw hook event types are best read as contracts. The event key tells you when the Gateway calls your code, how much it waits, what data you can trust, and whether replies have any route back to the user. Choose the narrowest event that matches the job, verify the real side effect on the Gateway host, and keep anything slow or durable out of the handler path.

For most teams, that discipline is the difference between hooks that quietly help operations and hooks that become another invisible source of state.

Suggested internal links: OpenClaw Hooks, OpenClaw Hook Configuration, OpenClaw Bundled Hooks.

Suggested external sources: https://docs.openclaw.ai/automation/hooks/event-types, https://docs.openclaw.ai/automation/hooks, https://docs.openclaw.ai/plugins/hooks

Image alt text: Diagram of OpenClaw hook event families showing command, session, agent, gateway, and message event paths inside the Gateway.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *