Writing hooks: file layout, handler contract, and replies

Covers the hook directory layout, the handler contract, reply delivery, and the HOOK.md metadata fields. Written for developers who want to build and load a custom hook on the Gateway host.

Read this when

  • You are writing a new internal hook and need the file layout
  • You need the handler signature, event fields, or reply-delivery boundary
  • You need the `HOOK.md` frontmatter and `metadata.openclaw` contract

Hook file layout, the handler contract, how replies are delivered, and the HOOK.md metadata fields. This page is part of the Hooks guide.

Writing hooks

The example below answers a reset command and writes a fixed log marker. It never reads message content, calls a model, or contacts an external service.

Hook structure

Create a fresh managed hook directory on the Gateway host. The commands that follow presume the default state directory and that reset-greeting does not exist yet; pick a different name instead of overwriting a hook that is already there.

mkdir -p ~/.openclaw/hooks/reset-greeting

cat > ~/.openclaw/hooks/reset-greeting/HOOK.md <<'HOOK'
---
name: reset-greeting
description: "Confirm that a reset hook ran"
metadata:
  { "openclaw": { "events": ["command:new", "command:reset"] } }
---

# Reset greeting

Send a short confirmation after an authorized reset command.
HOOK

cat > ~/.openclaw/hooks/reset-greeting/handler.js <<'HANDLER'
export default function handler(event) {
  if (event.type !== "command" || !["new", "reset"].includes(event.action)) {
    return;
  }

  console.log("[reset-greeting] reset hook ran");
  event.messages.push("Reset hook ran.");
}
HANDLER

Two things are required for a hook: HOOK.md and a handler file. Discovery looks for handler.ts, handler.js, index.ts, and then index.js, in that order, and takes the first file it finds. JavaScript is used in the example, so no TypeScript types or SDK imports are required.

Turn it on and load it:

openclaw hooks info reset-greeting
openclaw hooks enable reset-greeting

In a disposable conversation on a configured chat channel that can route replies, such as a direct message to the bot, send /new. Reset hook ran. should appear in that conversation, and [reset-greeting] reset hook ran should appear in Gateway logs. The same example is also triggered by /reset. Ordinary command authorization still applies.

Use a regular OpenClaw conversation rather than an ACP-bound thread, since bound sessions hand reset handling to the runtime that owns them. As the chat-reply check, avoid Control UI/webchat and a sessions.reset RPC: neither path delivers this hook's event.messages to the UI. The log marker can still confirm that a reset event ran. The precise boundary is described in Reply delivery.

When you are done with the example, disable it:

openclaw hooks disable reset-greeting

Files stay on disk after disabling. If you would rather use a workspace directory, place the two files in <workspace>/hooks/reset-greeting/ and then enable the hook explicitly. A workspace location is neither an agent sandbox nor a promise that the Gateway will load hooks from that workspace.

Handler implementation

A handler exports a function that returns void or Promise<void>. The default export is what the loader uses, unless metadata.openclaw.export names a different export. Nothing a handler returns blocks, cancels, or rewrites the operation.

These fields are present on every event:

FieldMeaning
typeFamily: command, session, agent, gateway, or message
actionAction within the family, such as new or compact:before
sessionKeySession correlation key; Gateway events use a Gateway key instead
timestampJavaScript Date when the event object was created
contextEvent-specific data described under Event context highlights
messagesInitially empty string array; only certain producers consume it as replies

Context is meant for observation, not as a live API for editing state. Which fields appear depends on the producer, and cfg is absent from some events. Patch events, for example, carry cloned snapshots. The one explicit mutable exception is agent:bootstrap's context.bootstrapFiles.

Reply delivery

Pushing to event.messages is not a general-purpose send-message API:

ProducerWhat happens to event.messages
Chat command handling for /new and /resetAwaits handlers, joins strings with blank lines, and attempts a reply to the originating channel/recipient, preserving account and thread context
Gateway session reset/create RPCs that emit command:new or command:resetHandlers run, but messages are not routed as chat replies
session:compact:before and session:compact:afterForwarded to the caller's compaction-notice callback when present; that callback owns delivery
All other core eventsIgnored as replies, including /stop, automatic reset, message events, bootstrap, patch, and Gateway lifecycle events

A reply can fail to arrive for several reasons: no recipient, an unsupported route, send policy, or a delivery error. Append messages before the handler's promise settles, because detached work that pushes afterward may miss the producer's delivery step. For control over normal agent replies or for sending a cancellation, use the appropriate typed plugin hook.

HOOK.md format

HOOK.md consists of YAML frontmatter followed by human-readable Markdown:

---
name: my-hook
description: "Short description of what this hook does"
homepage: https://example.com/my-hook
metadata:
  { "openclaw": { "emoji": "🔗", "events": ["command:new"], "requires": { "bins": ["node"] } } }
---

# My Hook

Explain the side effects, configuration, and verification steps here.

name defaults to the directory name, so choose a name that is unique and stable. description appears in reports. The fields listed below go under metadata.openclaw.

The metadata.clawdbot layout used before July 2026 is no longer parsed. To bring an older HOOK.md up to date, rename that metadata block to openclaw and leave its event list and remaining fields intact. When an openclaw block is already present, retain its values and merge in only the legacy fields you still need. OpenClaw leaves the file untouched, and any event declarations or requirements inside the old block are disregarded.

FieldContract
eventsArray of event keys. Registering a handler requires at least one entry.
exportName of the exported function; default is used when unspecified.
hookKeyKey for the config entry; the hook name is the default. Discovery collisions are still resolved by hook name.
emojiEmoji shown in the interface.
homepageURL for documentation; takes precedence over the top-level homepage, website, or url.
osNode platform names that are permitted, such as darwin, linux, or win32.
requires.binsEach executable you name has to be present on PATH.
requires.anyBinsAt least one of the named executables has to be present on PATH.
requires.envEach named variable requires a nonblank process value, or a per-hook env value.
requires.configEvery dotted config path has to evaluate as truthy.
alwaysSkips binary, environment, and config requirements; OS policy and enablement policy are still enforced.
installInformational install descriptors: kind takes the value bundled, npm, or git; optional keys are id, label, package, repository, and bins. This metadata neither installs dependencies nor causes the CLI to accept Git specs.

Activation is controlled through hooks.internal.entries.<hookKey>.enabled, not by a top-level enabled flag placed in HOOK.md. Where historical requirement metadata is concerned, workspace.dir, browser.enabled, and browser.evaluateEnabled are treated as true when they are missing. workspace.dir is not a new setting for you to add to your config.

1,164 words · updated Oct 26, 2026