Skip to content

[Bug]: user-plugin hook handlers do not dispatch at runtime despite ✓ ready in openclaw hooks list #85174

@DhillanC

Description

@DhillanC

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-WhatsApp voice notes fail without explicit "audio/ogg; codecs=opus" mimetype #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: Feature request: per-account and per-peer sendPolicy matching for shadow mode DM handling #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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:needs-product-decisionClawSweeper marked this issue as needing a product or behavior decision.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions