-
-
Notifications
You must be signed in to change notification settings - Fork 69.5k
[Bug]: Plugin api.on('command', ...) handlers never fire - triggerInternalHook and typedHooks are disconnected systems #25074
Description
Summary
Plugin hooks registered via api.on('command', ...) never fire because triggerInternalHook and the typed hook system (registry.typedHooks) are two completely separate dispatch systems with no bridge between them.
Steps to reproduce
- Write an OpenClaw plugin that registers a
commandhook:
register(api) {
api.on('command', async (event) => {
process.stderr.write(`[PROBE] command hook FIRED action=${event.action}\n`);
});
}- Install the plugin and restart the gateway.
- Hit
/newin any session (webchat, Telegram, etc.). - Check stderr -
[PROBE] command hook FIREDnever appears.
Expected behavior
When /new or /reset is issued, the gateway calls triggerInternalHook('command', ...) which should dispatch to any plugin handlers registered via api.on('command', ...).
Actual behavior
The handler never fires. api.on('command', ...) pushes to registry.typedHooks[] (System 2), but triggerInternalHook only reads from the module-level handlers Map in internal-hooks.ts (System 1). The two systems have no bridge.
Root cause (traced to source)
There are two independent hook dispatch systems:
System 1 - Internal hooks (subsystem-CYc2uK4P.js ~line 29):
const handlers = new Map(); // module-level
function registerInternalHook(eventKey, handler) {
if (!handlers.has(eventKey)) handlers.set(eventKey, []);
handlers.get(eventKey).push(handler);
}
async function triggerInternalHook(event) {
const typeHandlers = handlers.get(event.type) ?? [];
// only reads from 'handlers' Map - never touches registry.typedHooks
}System 2 - Typed plugin hooks (subsystem-CYc2uK4P.js ~line 522):
const registerTypedHook = (record, hookName, handler, opts) => {
registry.typedHooks.push({ pluginId, hookName, handler, ... });
// stored in registry.typedHooks[] - never bridged to System 1
};
// api.on() calls registerTypedHookThe disconnect: When sessions.reset fires triggerInternalHook('command', ...), it dispatches from System 1. Plugin handlers registered via api.on('command', ...) live in System 2. They never meet.
The only bridge between the two systems is in the older registerHook() path (~line 371), which calls registerInternalHook after checking config.hooks.internal.enabled === true. api.on() (registerTypedHook) has no such bridge.
Additionally, 'command' is not in the PluginHookName union type and the hookRunner has no runCommand() method, so there is no typed hook dispatch path for command events at all.
Impact
Plugins cannot intercept session reset events (/new / /reset) from the gateway RPC path. This makes it impossible for plugins to perform pre-reset work (summarization, handoff storage, cleanup) when a session is reset via webchat or any other RPC-originated path.
The before_reset hook fires correctly for chat-originated /new (the auto-reply path), but NOT for RPC-originated resets - meaning there is no reliable hook available to plugins for session reset events.
Suggested fix
Two options:
Option A - Bridge triggerInternalHook to typed hooks: when dispatching, also call any registry.typedHooks entries whose hookName matches event.type.
Option B - Add 'command' to PluginHookName, add hookRunner.runCommand(), and call it from sessions.reset in addition to (or instead of) triggerInternalHook.
Option B is cleaner as it brings command events into the same typed system as all other plugin lifecycle hooks.
Environment
- OpenClaw: 2026.2.22-2
- Node: v24.13.0
- macOS 25.2.0 (arm64)