openclaw - 💡(How to fix) Fix [Bug]: user-plugin hook handlers do not dispatch at runtime despite `✓ ready` in `openclaw hooks list`

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

On openclaw 2026.5.19 (a185ca2, npm-installed), user-plugin hook handlers registered via api.registerHook(eventName, fn, meta) from plugins under ~/.openclaw/extensions/<id>/ show as ✓ ready in openclaw hooks list but the gateway never invokes them when the corresponding event fires. Reproduced with a minimal probe plugin (stderr-verify) registering gateway_start and before_agent_run; both register cleanly across multiple boot/reload cycles, neither executes during real channel turns. Observers registered via api.observe(...) from the same install path do fire normally (verified by message-audit's JSONL growing on the same turn).

Error Message

  1. Create a minimal probe plugin (full source below) at ~/.openclaw/plugins/stderr-verify/ registering two hooks — gateway_start and before_agent_run — each appending a line to ~/.openclaw/stderr-verify-proof.txt from inside the handler body, plus emitting via api.logger.error/warn/info, console.error, and process.stderr.write. The plugin's register(api) callback writes a separate register() called line to the same file so registration can be distinguished from dispatch. log.error?.(${MARKER} hook=${label} path=api.logger.error ts=${ts}); log.warn?.(${MARKER} hook=${label} path=api.logger.warn ts=${ts}); console.error(${MARKER} hook=${label} path=console.error ts=${ts});
  • grep -c STDERR_VERIFY ~/Library/Logs/openclaw/gateway.err.log returned 0. None of the five emit paths (api.logger.error/warn/info, console.error, process.stderr.write) produced a marker. Since each path writes from inside the handler body, this independently confirms the handler did not run (had the body executed, at least one path would have left evidence; appendFileSync writes via OS fd independent of stderr routing, ruling out a stderr-sink explanation).

Root Cause

  • openclaw hooks list shows Hooks (N/N ready) with N including the probe hooks throughout, so the count itself is misleading as a liveness indicator — registered ≠ dispatched.
  • The bundled reply-only-hook plugin (cause-#7 defense in this install's hardening track) uses the same api.registerHook surface; its gateway_start startup-invariant log line "reply-only-hook armed (dmScope=...)" is absent from every log file across the whole day, consistent with the same gap. The 26-case smoke test for that plugin passes because it calls handler functions directly with mocked api/ctx; it does not exercise gateway dispatch.
  • Suspected root cause space (not investigated upstream-side): the source: plugin:* registration path may not be wired into the same dispatcher as source: openclaw-bundled; or api.registerHook from a plugin module may register into a registry the dispatcher does not consult; or there is a config gate I have not found. Happy to add diagnostic output to the probe plugin if a maintainer wants to direct the next step.
  • Reference: #66938 closure noted that sendPolicy is intentionally prefix-based — this report is independent of that schema question; it is about hook dispatch, not policy semantics.

Fix Action

Fix / Workaround

  1. Install OpenClaw via npm install -g openclaw@latest (2026.5.19, a185ca2).
  2. Create a minimal probe plugin (full source below) at ~/.openclaw/plugins/stderr-verify/ registering two hooks — gateway_start and before_agent_run — each appending a line to ~/.openclaw/stderr-verify-proof.txt from inside the handler body, plus emitting via api.logger.error/warn/info, console.error, and process.stderr.write. The plugin's register(api) callback writes a separate register() called line to the same file so registration can be distinguished from dispatch.
  3. Install: openclaw plugins install ~/.openclaw/plugins/stderr-verify.
  4. Restart: launchctl kickstart -k gui/501/ai.openclaw.gateway (macOS) or equivalent.
  5. Confirm registration: openclaw hooks list shows stderr-verify-gateway-start and stderr-verify-before-agent-run as ✓ ready.
  6. Send a real channel message (Telegram or WhatsApp DM) to the agent; the agent processes and replies (confirmed by message-audit JSONL appending dir:"in" + dir:"out" paired entries with success:true).
  7. Inspect ~/.openclaw/stderr-verify-proof.txt and ~/Library/Logs/openclaw/gateway.err.log.

export default { id: "stderr-verify", name: "Stderr Verify (PROBE)", description: "Probes hook dispatch + logger paths.", configSchema: { type: "object", additionalProperties: false, properties: {} }, register(api) { appendProof(${MARKER} register() called); const log = api.logger ?? console; function fire(label) { const ts = Date.now(); appendProof(${MARKER} ${label} hook fired ts=${ts}); log.error?.(${MARKER} hook=${label} path=api.logger.error ts=${ts}); log.warn?.(${MARKER} hook=${label} path=api.logger.warn ts=${ts}); log.info?.(${MARKER} hook=${label} path=api.logger.info ts=${ts}); console.error(${MARKER} hook=${label} path=console.error ts=${ts}); try { process.stderr.write(${MARKER} hook=${label} path=process.stderr.write ts=${ts}\n); } catch {} } api.registerHook("gateway_start", async () => fire("gateway_start"), { name: "stderr-verify-gateway-start", description: "Probe gateway_start dispatch." }); api.registerHook("before_agent_run", async () => fire("before_agent_run"), { name: "stderr-verify-before-agent-run", description: "Probe before_agent_run dispatch." }); }, };


- `message-audit` plugin (also installed under `~/.openclaw/extensions/`, uses `api.observe('message:received', ...)` / `api.observe('message:sent', ...)`) appends to its JSONL on every turn — confirmed for the same Telegram round-trips above.
- Channel plugins (`[telegram]`, `[whatsapp]`) emit `[diag]` lines to `gateway.err.log` normally.
- Bundled hooks (`source: openclaw-bundled`, e.g. `bootstrap-extra-files`) produce visible side effects (`workspace bootstrap file AGENTS.md is 16780 chars (limit 12000); truncating in injected context`), suggesting they do dispatch — only `source: plugin:*` user-installed hooks are affected.

Code Example

import { appendFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";

const MARKER = "STDERR_VERIFY_2026-05-21";
const PROOF_FILE = join(homedir(), ".openclaw", "stderr-verify-proof.txt");

function appendProof(line) {
  try { appendFileSync(PROOF_FILE, `${new Date().toISOString()} ${line}\n`); } catch {}
}

export default {
  id: "stderr-verify",
  name: "Stderr Verify (PROBE)",
  description: "Probes hook dispatch + logger paths.",
  configSchema: { type: "object", additionalProperties: false, properties: {} },
  register(api) {
    appendProof(`${MARKER} register() called`);
    const log = api.logger ?? console;
    function fire(label) {
      const ts = Date.now();
      appendProof(`${MARKER} ${label} hook fired ts=${ts}`);
      log.error?.(`${MARKER} hook=${label} path=api.logger.error ts=${ts}`);
      log.warn?.(`${MARKER}  hook=${label} path=api.logger.warn ts=${ts}`);
      log.info?.(`${MARKER}  hook=${label} path=api.logger.info ts=${ts}`);
      console.error(`${MARKER} hook=${label} path=console.error ts=${ts}`);
      try { process.stderr.write(`${MARKER} hook=${label} path=process.stderr.write ts=${ts}\n`); } catch {}
    }
    api.registerHook("gateway_start",     async () => fire("gateway_start"),     { name: "stderr-verify-gateway-start",     description: "Probe gateway_start dispatch." });
    api.registerHook("before_agent_run",  async () => fire("before_agent_run"),  { name: "stderr-verify-before-agent-run",  description: "Probe before_agent_run dispatch." });
  },
};

---

{ "id": "stderr-verify", "name": "Stderr Verify (PROBE)", "version": "0.0.1", "main": "./index.mjs" }

---

{ "name": "stderr-verify", "version": "0.0.1", "type": "module", "openclaw": { "extensions": ["./index.mjs"] } }
RAW_BUFFERClick to expand / collapse

Summary

On openclaw 2026.5.19 (a185ca2, npm-installed), user-plugin hook handlers registered via api.registerHook(eventName, fn, meta) from plugins under ~/.openclaw/extensions/<id>/ show as ✓ ready in openclaw hooks list but the gateway never invokes them when the corresponding event fires. Reproduced with a minimal probe plugin (stderr-verify) registering gateway_start and before_agent_run; both register cleanly across multiple boot/reload cycles, neither executes during real channel turns. Observers registered via api.observe(...) from the same install path do fire normally (verified by message-audit's JSONL growing on the same turn).

Steps to reproduce

  1. Install OpenClaw via npm install -g openclaw@latest (2026.5.19, a185ca2).
  2. Create a minimal probe plugin (full source below) at ~/.openclaw/plugins/stderr-verify/ registering two hooks — gateway_start and before_agent_run — each appending a line to ~/.openclaw/stderr-verify-proof.txt from inside the handler body, plus emitting via api.logger.error/warn/info, console.error, and process.stderr.write. The plugin's register(api) callback writes a separate register() called line to the same file so registration can be distinguished from dispatch.
  3. Install: openclaw plugins install ~/.openclaw/plugins/stderr-verify.
  4. Restart: launchctl kickstart -k gui/501/ai.openclaw.gateway (macOS) or equivalent.
  5. Confirm registration: openclaw hooks list shows stderr-verify-gateway-start and stderr-verify-before-agent-run as ✓ ready.
  6. Send a real channel message (Telegram or WhatsApp DM) to the agent; the agent processes and replies (confirmed by message-audit JSONL appending dir:"in" + dir:"out" paired entries with success:true).
  7. Inspect ~/.openclaw/stderr-verify-proof.txt and ~/Library/Logs/openclaw/gateway.err.log.

Probe plugin source (index.mjs):

import { appendFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";

const MARKER = "STDERR_VERIFY_2026-05-21";
const PROOF_FILE = join(homedir(), ".openclaw", "stderr-verify-proof.txt");

function appendProof(line) {
  try { appendFileSync(PROOF_FILE, `${new Date().toISOString()} ${line}\n`); } catch {}
}

export default {
  id: "stderr-verify",
  name: "Stderr Verify (PROBE)",
  description: "Probes hook dispatch + logger paths.",
  configSchema: { type: "object", additionalProperties: false, properties: {} },
  register(api) {
    appendProof(`${MARKER} register() called`);
    const log = api.logger ?? console;
    function fire(label) {
      const ts = Date.now();
      appendProof(`${MARKER} ${label} hook fired ts=${ts}`);
      log.error?.(`${MARKER} hook=${label} path=api.logger.error ts=${ts}`);
      log.warn?.(`${MARKER}  hook=${label} path=api.logger.warn ts=${ts}`);
      log.info?.(`${MARKER}  hook=${label} path=api.logger.info ts=${ts}`);
      console.error(`${MARKER} hook=${label} path=console.error ts=${ts}`);
      try { process.stderr.write(`${MARKER} hook=${label} path=process.stderr.write ts=${ts}\n`); } catch {}
    }
    api.registerHook("gateway_start",     async () => fire("gateway_start"),     { name: "stderr-verify-gateway-start",     description: "Probe gateway_start dispatch." });
    api.registerHook("before_agent_run",  async () => fire("before_agent_run"),  { name: "stderr-verify-before-agent-run",  description: "Probe before_agent_run dispatch." });
  },
};

Manifest (openclaw.plugin.json):

{ "id": "stderr-verify", "name": "Stderr Verify (PROBE)", "version": "0.0.1", "main": "./index.mjs" }

package.json:

{ "name": "stderr-verify", "version": "0.0.1", "type": "module", "openclaw": { "extensions": ["./index.mjs"] } }

Expected behavior

Per the SDK contract that register() calls api.registerHook(eventName, handler, meta) and that handlers run when the named event fires:

  • gateway_start handler should execute on every gateway boot / config-reload cycle. ~/.openclaw/stderr-verify-proof.txt should contain at least one gateway_start hook fired ts=... line per boot. ~/Library/Logs/openclaw/gateway.err.log should contain at least one STDERR_VERIFY_* marker per boot.
  • before_agent_run handler should execute on every agent turn (including channel-triggered turns). The proof file should grow by at least one before_agent_run hook fired line per processed inbound message.

This matches the documented hook lifecycle in the bundled plugin SDK types (PluginHookAgentContext is delivered to before_agent_run handlers; PluginHookMessageContext to message_sending handlers). It also matches how the bundled reply-only-hook plugin in the same install is documented to work (its gateway_start handler logs "reply-only-hook armed (dmScope=...)" — that string never appears in any log file on this install).

Actual behavior

Across multiple boot/reload cycles and at least two real Telegram inbound round-trips (both with paired success:true audit entries showing the agent replied), the probe handler body never executed.

Concretely, after a Telegram round-trip on 2026-05-22 UTC:

  • ~/.openclaw/audit/messages.2026-05-22.jsonl gained 4 lines (2 inbound, 2 outbound, all dir/channel/peer/sessionKey correct, success:true). Observers fire.
  • ~/.openclaw/stderr-verify-proof.txt contained only register() called lines (3 of them, across boots and a config-reload — one of which landed between the two inbound messages, so at least the second turn definitively post-dated the most recent registration). No hook fired lines.
  • grep -c STDERR_VERIFY ~/Library/Logs/openclaw/gateway.err.log returned 0. None of the five emit paths (api.logger.error/warn/info, console.error, process.stderr.write) produced a marker. Since each path writes from inside the handler body, this independently confirms the handler did not run (had the body executed, at least one path would have left evidence; appendFileSync writes via OS fd independent of stderr routing, ruling out a stderr-sink explanation).
  • openclaw hooks list continued to show both handlers as ✓ ready throughout.

Same behavior observed when triggering via openclaw agent --agent main -m "..." CLI — handler body does not execute despite the agent producing a full reply.

Control evidence that the channel pipeline and observer surface are functioning normally:

  • message-audit plugin (also installed under ~/.openclaw/extensions/, uses api.observe('message:received', ...) / api.observe('message:sent', ...)) appends to its JSONL on every turn — confirmed for the same Telegram round-trips above.
  • Channel plugins ([telegram], [whatsapp]) emit [diag] lines to gateway.err.log normally.
  • Bundled hooks (source: openclaw-bundled, e.g. bootstrap-extra-files) produce visible side effects (workspace bootstrap file AGENTS.md is 16780 chars (limit 12000); truncating in injected context), suggesting they do dispatch — only source: plugin:* user-installed hooks are affected.

OpenClaw version

2026.5.19 (a185ca2)

Operating system

macOS 15.6 (Darwin 24.6.0)

Install method

npm global — npm install -g openclaw@latest. /opt/homebrew/bin/openclaw is a symlink to /opt/homebrew/lib/node_modules/openclaw/openclaw.mjs.

Model

moonshotai/kimi-k2-instruct (primary), openrouter/anthropic/claude-sonnet-4.6 (fallback).

Provider / routing chain

openclaw → openrouter → moonshotai (primary); fallback openclaw → openrouter → anthropic.

Additional context

  • openclaw hooks list shows Hooks (N/N ready) with N including the probe hooks throughout, so the count itself is misleading as a liveness indicator — registered ≠ dispatched.
  • The bundled reply-only-hook plugin (cause-#7 defense in this install's hardening track) uses the same api.registerHook surface; its gateway_start startup-invariant log line "reply-only-hook armed (dmScope=...)" is absent from every log file across the whole day, consistent with the same gap. The 26-case smoke test for that plugin passes because it calls handler functions directly with mocked api/ctx; it does not exercise gateway dispatch.
  • Suspected root cause space (not investigated upstream-side): the source: plugin:* registration path may not be wired into the same dispatcher as source: openclaw-bundled; or api.registerHook from a plugin module may register into a registry the dispatcher does not consult; or there is a config gate I have not found. Happy to add diagnostic output to the probe plugin if a maintainer wants to direct the next step.
  • Reference: #66938 closure noted that sendPolicy is intentionally prefix-based — this report is independent of that schema question; it is about hook dispatch, not policy semantics.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

Per the SDK contract that register() calls api.registerHook(eventName, handler, meta) and that handlers run when the named event fires:

  • gateway_start handler should execute on every gateway boot / config-reload cycle. ~/.openclaw/stderr-verify-proof.txt should contain at least one gateway_start hook fired ts=... line per boot. ~/Library/Logs/openclaw/gateway.err.log should contain at least one STDERR_VERIFY_* marker per boot.
  • before_agent_run handler should execute on every agent turn (including channel-triggered turns). The proof file should grow by at least one before_agent_run hook fired line per processed inbound message.

This matches the documented hook lifecycle in the bundled plugin SDK types (PluginHookAgentContext is delivered to before_agent_run handlers; PluginHookMessageContext to message_sending handlers). It also matches how the bundled reply-only-hook plugin in the same install is documented to work (its gateway_start handler logs "reply-only-hook armed (dmScope=...)" — that string never appears in any log file on this install).

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix [Bug]: user-plugin hook handlers do not dispatch at runtime despite `✓ ready` in `openclaw hooks list`