openclaw - 💡(How to fix) Fix ensureRuntimePluginsLoaded() can overwrite global hook runner during message processing [1 participants]

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…
GitHub stats
openclaw/openclaw#61551Fetched 2026-04-08 02:57:26
View on GitHub
Comments
0
Participants
1
Timeline
0
Reactions
0
Participants

During message processing, ensureRuntimePluginsLoaded() can be called, which triggers the full plugin loading sequence: loadOpenClawPlugins()activatePluginRegistry()initializeGlobalHookRunner(registry). This replaces the global hook runner (stored at globalThis[Symbol.for("openclaw.plugins.hook-runner-global-state")]) with a new one that may not contain the hooks from the gateway's plugin registry.

Root Cause

During message processing, ensureRuntimePluginsLoaded() can be called, which triggers the full plugin loading sequence: loadOpenClawPlugins()activatePluginRegistry()initializeGlobalHookRunner(registry). This replaces the global hook runner (stored at globalThis[Symbol.for("openclaw.plugins.hook-runner-global-state")]) with a new one that may not contain the hooks from the gateway's plugin registry.

RAW_BUFFERClick to expand / collapse

Description

During message processing, ensureRuntimePluginsLoaded() can be called, which triggers the full plugin loading sequence: loadOpenClawPlugins()activatePluginRegistry()initializeGlobalHookRunner(registry). This replaces the global hook runner (stored at globalThis[Symbol.for("openclaw.plugins.hook-runner-global-state")]) with a new one that may not contain the hooks from the gateway's plugin registry.

Background

On gateway startup, plugins load twice:

  1. [plugins] subsystem creates a registry and calls initializeGlobalHookRunner()
  2. [gateway] subsystem creates a second registry and overwrites the global hook runner

Both registries contain the same hooks, so the second overwrite is harmless.

However, if ensureRuntimePluginsLoaded() is called during message processing (e.g., during an agent turn), it can create a third registry via loadOpenClawPlugins() and overwrite the global hook runner again. If this new registry doesn't contain the same hooks (e.g., due to timing or registration order), hooks that were previously registered become unreachable.

Expected Behavior

The global hook runner established during gateway startup should remain stable throughout the gateway's lifetime. Runtime plugin loading should either:

  • Not overwrite the existing hook runner if one already exists, or
  • Merge hooks from the new registry into the existing one

Actual Behavior

initializeGlobalHookRunner(registry) unconditionally replaces the global hook runner, regardless of whether one already exists with registered hooks.

Impact

This can cause message_sending (and potentially other) hooks to silently stop firing during the gateway's lifetime, even though they were correctly registered at startup.

Relevant Code

  • initializeGlobalHookRunner(registry) — unconditionally sets state.hookRunner = createHookRunner(registry)
  • getGlobalHookRunner() — reads from global state, returns whatever was last set
  • ensureRuntimePluginsLoaded() — can trigger the overwrite during message processing

Environment

  • OpenClaw version: 2026.3.13 (61d171a)
  • Node.js: 22.22.1

Related

This bug was discovered while investigating #61550 (message_sending hooks bypassed with streaming). Even with streaming: "off", this registry overwrite can prevent hooks from firing intermittently.

extent analysis

TL;DR

Modify initializeGlobalHookRunner(registry) to merge new hooks into the existing global hook runner instead of overwriting it.

Guidance

  • Check the initializeGlobalHookRunner(registry) function to see if it can be modified to check for an existing global hook runner before overwriting it.
  • Consider adding a check in ensureRuntimePluginsLoaded() to prevent overwriting the global hook runner during message processing if it already exists.
  • Verify that the global hook runner is being overwritten by adding logging or debugging statements in initializeGlobalHookRunner(registry) and getGlobalHookRunner().
  • Review the plugin registration order and timing to ensure that all necessary hooks are registered before ensureRuntimePluginsLoaded() is called.

Example

// Modified initializeGlobalHookRunner function
function initializeGlobalHookRunner(registry) {
  const existingHookRunner = getGlobalHookRunner();
  if (existingHookRunner) {
    // Merge new hooks into the existing hook runner
    existingHookRunner.mergeHooks(registry);
  } else {
    // Create a new hook runner if one doesn't exist
    state.hookRunner = createHookRunner(registry);
  }
}

Notes

The exact implementation of the mergeHooks function is not specified, as it depends on the internal workings of the createHookRunner function and the registry object.

Recommendation

Apply a workaround by modifying the initializeGlobalHookRunner(registry) function to merge new hooks into the existing global hook runner, as this will prevent the overwriting of existing hooks and ensure that all registered hooks are fired correctly.

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…

Still need to ship something?

×6

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

Back to top recommendations

TRENDING