openclaw - 💡(How to fix) Fix Plugin loader silently default-denies conversation-hook registration; no log output, only buried diagnostic

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…

When a non-bundled plugin registers a typed conversation hook (llm_input, llm_output, before_agent_finalize, agent_end) without plugins.entries.<id>.hooks.allowConversationAccess: true, the loader silently default-denies registration. The drop is recorded only via pushDiagnostic({ level: "warn", ... }), which goes into a per-plugin diagnostics sink visible only through openclaw plugins inspect <id> --runtime --json. Nothing is printed to the gateway log, no warning surfaces in openclaw doctor, and the plugin author has no way to discover the gate from operational signals — code paths that look correct will simply never execute.

This burned us today (2026-05-08) on @openclaw/memory-continuity Phase 1 cutover: the llm_output finalize handler was registered correctly in plugin code and even passed our integration tests, but in production its body never executed because the loader silently dropped registration. We only found it by reading the dist source and running openclaw plugins inspect memory-continuity --runtime --json. The diagnostic has been there the whole time; we just had no signal pointing us at it.

Error Message

When a non-bundled plugin registers a typed conversation hook (llm_input, llm_output, before_agent_finalize, agent_end) without plugins.entries.<id>.hooks.allowConversationAccess: true, the loader silently default-denies registration. The drop is recorded only via pushDiagnostic({ level: "warn", ... }), which goes into a per-plugin diagnostics sink visible only through openclaw plugins inspect <id> --runtime --json. Nothing is printed to the gateway log, no warning surfaces in openclaw doctor, and the plugin author has no way to discover the gate from operational signals — code paths that look correct will simply never execute.

level: "warn",

level: "warn", Promote conversation-hook denials from pushDiagnostic({ level: "warn" }) to a real console.warn (or the equivalent gateway-logger logger.warn) call in addition to the diagnostic. Suggested message format: Optional second improvement: have openclaw doctor surface plugin diagnostics with level: "warn" or higher. That way operators get a single command to find these issues.

  • Plugin policy reference: docs/plugins/policy.md documents the field but doesn't warn that lack of it produces silent failure.

Root Cause

This burned us today (2026-05-08) on @openclaw/memory-continuity Phase 1 cutover: the llm_output finalize handler was registered correctly in plugin code and even passed our integration tests, but in production its body never executed because the loader silently dropped registration. We only found it by reading the dist source and running openclaw plugins inspect memory-continuity --runtime --json. The diagnostic has been there the whole time; we just had no signal pointing us at it.

Fix Action

Workaround

Set plugins.entries.<id>.hooks.allowConversationAccess: true in openclaw.json. This is documented on the plugin-policy reference page but is easy to miss when authoring a new plugin from scratch.

Code Example

# 1. Author a non-bundled plugin that registers llm_output:
cat > /tmp/repro-plugin/index.js <<'EOF'
module.exports = function init(ctx) {
  ctx.hooks.register("llm_output", async (event) => {
    // This handler will silently never be called.
    require("fs").appendFileSync("/tmp/repro-plugin.log", "fired\n");
  });
};
EOF

# 2. Register it in openclaw.json without `allowConversationAccess`:
#    plugins.entries.repro = { path: "/tmp/repro-plugin", config: {}, hooks: {} }

# 3. Restart gateway, exercise a turn.

# 4. Observe:
#    - /tmp/repro-plugin.log: never created
#    - openclaw gateway logs: NO warning about the dropped hook
#    - openclaw doctor: NO warning
#    - openclaw plugins inspect repro --runtime --json:
#        diagnostics: [{
#          level: "warn",
#          message: "typed hook \"llm_output\" blocked because non-bundled plugins must set plugins.entries.repro.hooks.allowConversationAccess=true"
#        }]

---

if (isConversationHookName(hookName)) {
  const explicitConversationAccess = policy?.allowConversationAccess;
  if (record.origin !== "bundled" && explicitConversationAccess !== true) {
    pushDiagnostic({
      level: "warn",
      pluginId: record.id,
      source: record.source,
      message: `typed hook "${hookName}" blocked because non-bundled plugins must set plugins.entries.${record.id}.hooks.allowConversationAccess=true`
    });
    return;
  }
  ...
}

---

[plugin:<id>] hook "<hookName>" registration blocked: non-bundled plugins must set plugins.entries.<id>.hooks.allowConversationAccess=true. Handler will not be invoked.
RAW_BUFFERClick to expand / collapse

Summary

When a non-bundled plugin registers a typed conversation hook (llm_input, llm_output, before_agent_finalize, agent_end) without plugins.entries.<id>.hooks.allowConversationAccess: true, the loader silently default-denies registration. The drop is recorded only via pushDiagnostic({ level: "warn", ... }), which goes into a per-plugin diagnostics sink visible only through openclaw plugins inspect <id> --runtime --json. Nothing is printed to the gateway log, no warning surfaces in openclaw doctor, and the plugin author has no way to discover the gate from operational signals — code paths that look correct will simply never execute.

This burned us today (2026-05-08) on @openclaw/memory-continuity Phase 1 cutover: the llm_output finalize handler was registered correctly in plugin code and even passed our integration tests, but in production its body never executed because the loader silently dropped registration. We only found it by reading the dist source and running openclaw plugins inspect memory-continuity --runtime --json. The diagnostic has been there the whole time; we just had no signal pointing us at it.

Reproduction

# 1. Author a non-bundled plugin that registers llm_output:
cat > /tmp/repro-plugin/index.js <<'EOF'
module.exports = function init(ctx) {
  ctx.hooks.register("llm_output", async (event) => {
    // This handler will silently never be called.
    require("fs").appendFileSync("/tmp/repro-plugin.log", "fired\n");
  });
};
EOF

# 2. Register it in openclaw.json without `allowConversationAccess`:
#    plugins.entries.repro = { path: "/tmp/repro-plugin", config: {}, hooks: {} }

# 3. Restart gateway, exercise a turn.

# 4. Observe:
#    - /tmp/repro-plugin.log: never created
#    - openclaw gateway logs: NO warning about the dropped hook
#    - openclaw doctor: NO warning
#    - openclaw plugins inspect repro --runtime --json:
#        diagnostics: [{
#          level: "warn",
#          message: "typed hook \"llm_output\" blocked because non-bundled plugins must set plugins.entries.repro.hooks.allowConversationAccess=true"
#        }]

Source pointer

This is dist/loader-B-GXgDrk.js:2754 (in v2026.5.7, hash eeef486):

if (isConversationHookName(hookName)) {
  const explicitConversationAccess = policy?.allowConversationAccess;
  if (record.origin !== "bundled" && explicitConversationAccess !== true) {
    pushDiagnostic({
      level: "warn",
      pluginId: record.id,
      source: record.source,
      message: `typed hook "${hookName}" blocked because non-bundled plugins must set plugins.entries.${record.id}.hooks.allowConversationAccess=true`
    });
    return;
  }
  ...
}

The diagnostic sink is at dist/loader-B-GXgDrk.js:1313 and is a plugin-scoped buffer; it never reaches the gateway logger or stderr. CONVERSATION_HOOK_NAMES is defined at dist/types-CdFhLeaX.js:437.

Why this is bad UX

  1. Silent failures are the worst class of bug. A plugin that registers a conversation hook with no opt-in passes all in-process tests (handler is registered into the plugin's own callback table) but produces no behavior in production. There is no observable failure signal short of grepping dist source.
  2. Asymmetry with prompt-injection hooks. Prompt-injection hooks default-allow with allowPromptInjection: true implicit; conversation hooks default-deny. That's a defensible policy, but the asymmetry should be loudly visible.
  3. Diagnostics-only signal is ineffective. Plugin authors don't habitually run plugins inspect --runtime --json after every restart. Operators don't either. The diagnostic exists but nobody sees it.

Suggested fix

Promote conversation-hook denials from pushDiagnostic({ level: "warn" }) to a real console.warn (or the equivalent gateway-logger logger.warn) call in addition to the diagnostic. Suggested message format:

[plugin:<id>] hook "<hookName>" registration blocked: non-bundled plugins must set plugins.entries.<id>.hooks.allowConversationAccess=true. Handler will not be invoked.

This is consistent with how the loader already logs other plugin-load-time failures (missing modules, validation errors). Conversation-hook gating is a load-time decision, not a per-event decision, so a single startup warning per plugin/hook is appropriate and not noisy.

Optional second improvement: have openclaw doctor surface plugin diagnostics with level: "warn" or higher. That way operators get a single command to find these issues.

Workaround

Set plugins.entries.<id>.hooks.allowConversationAccess: true in openclaw.json. This is documented on the plugin-policy reference page but is easy to miss when authoring a new plugin from scratch.

Environment

  • OpenClaw 2026.5.7 (eeef486)
  • Linux 6.8.0-111-generic x64, Node v22.22.2
  • Plugin: @openclaw/memory-continuity (Phase 1 build, non-bundled)

Related

  • Plugin policy reference: docs/plugins/policy.md documents the field but doesn't warn that lack of it produces silent failure.
  • The same default-deny applies to agent_end, llm_input, before_agent_finalize, and llm_output per CONVERSATION_HOOK_NAMES at dist/types-CdFhLeaX.js:437.

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

openclaw - 💡(How to fix) Fix Plugin loader silently default-denies conversation-hook registration; no log output, only buried diagnostic