openclaw - 💡(How to fix) Fix [Bug]: Plugin `before_tool_call` hook does not fire for native exec on 2026.4.29 (Anthropic harness) [1 comments, 2 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#76201Fetched 2026-05-03 04:40:54
View on GitHub
Comments
1
Participants
2
Timeline
2
Reactions
2
Author
Timeline (top)
commented ×1unsubscribed ×1

A plugin that registers a before_tool_call hook never sees that handler invoked when a native tool (e.g. exec) runs through the Anthropic harness on 2026.4.29 (commit a448042). The plugin's register() runs, the same plugin's after_tool_call handler does fire, but before_tool_call is silently skipped — so plugins cannot block or modify native tool calls before execution.

Error Message

} catch (e) { console.error(e); }

Root Cause

The most likely root cause is at src/agents/pi-tools.before-tool-call.ts:528: the getGlobalHookRunner() call at line 474 either returns null or returns a runner whose registry.typedHooks does not include the plugin's handler in the runtime context the native-exec dispatch path uses. The short-circuit at line 528 then returns { blocked: false } without ever invoking hookRunner.runBeforeToolCall. We have not pinpointed whether this is a runtime-mode issue around activatePluginRegistry / preserveGatewayHookRunner (src/plugins/loader.ts:1411-1425), a singleton-init ordering bug, or something else; but the empirical signal is that before_tool_call is silent while after_tool_call works in the same plugin and same gateway PID.

Fix Action

Fix / Workaround

The most likely root cause is at src/agents/pi-tools.before-tool-call.ts:528: the getGlobalHookRunner() call at line 474 either returns null or returns a runner whose registry.typedHooks does not include the plugin's handler in the runtime context the native-exec dispatch path uses. The short-circuit at line 528 then returns { blocked: false } without ever invoking hookRunner.runBeforeToolCall. We have not pinpointed whether this is a runtime-mode issue around activatePluginRegistry / preserveGatewayHookRunner (src/plugins/loader.ts:1411-1425), a singleton-init ordering bug, or something else; but the empirical signal is that before_tool_call is silent while after_tool_call works in the same plugin and same gateway PID.

  • before_tool_call is not in CONVERSATION_HOOK_NAMES so the allowConversationAccess policy gate does not apply.
  • The same plugin's after_tool_call handler does fire on the same exec turns — the plugin code path and registration are reaching the registry; only the dispatch-time read of plugin handlers for before_tool_call is producing zero hits.
  • Local codex review was not run (codex CLI not installed on the repro machine). Happy to add a regression test alongside a fix once a maintainer confirms the diagnosis direction.

Code Example

api.on("before_tool_call", (event, ctx) => {
     fs.appendFileSync("/tmp/btc-trace.log", JSON.stringify({ts: Date.now(), tool: event.toolName}) + "\n");
   }, { priority: 50 });

---

# Plugin register() — fires once at gateway start
2026-05-02 13:39:21 [skill-channel-gate] register() called; rules=1

# Real exec turn — Discord HITL, agent returned correct ls output
2026-05-02 13:48:25  user: @Juno run ls
2026-05-02 13:48:44  Juno: AGENTS.md  CLAUDE.md  HEARTBEAT.md  IDENTITY.md  NOTES.md  README.md  SOUL.md  TOOLS.md  USER.md  docs/  plugins/  skills/

# Trace handler output — never written (file remained 0 bytes, mtime = pre-test truncate)
$ wc -c /tmp/skill-channel-gate-trace.log
0 /tmp/skill-channel-gate-trace.log

# No "[skill-channel-gate] before_tool_call" line ever appears in journal
$ journalctl --user -u openclaw-gateway --since "13:48" | grep skill-channel-gate
2026-05-02 13:39:21 [skill-channel-gate] register() called; rules=1

---

api.on("before_tool_call", (event, ctx) => {
  try {
    appendFileSync("/tmp/skill-channel-gate-trace.log",
      JSON.stringify({ts: Date.now(), pid: process.pid, toolName: event?.toolName, sessionKey: ctx?.sessionKey, paramsKeys: Object.keys(event?.params ?? {})}) + "\n");
    console.log(`[skill-channel-gate] before_tool_call: toolName=${event?.toolName}`);
  } catch (e) { console.error(e); }
  return undefined;
}, { priority: 50 });
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

A plugin that registers a before_tool_call hook never sees that handler invoked when a native tool (e.g. exec) runs through the Anthropic harness on 2026.4.29 (commit a448042). The plugin's register() runs, the same plugin's after_tool_call handler does fire, but before_tool_call is silently skipped — so plugins cannot block or modify native tool calls before execution.

Steps to reproduce

  1. Install OpenClaw 2026.4.29 globally (npm install -g [email protected]).
  2. Drop a minimal plugin into the workspace plugins/ dir whose register() only does:
    api.on("before_tool_call", (event, ctx) => {
      fs.appendFileSync("/tmp/btc-trace.log", JSON.stringify({ts: Date.now(), tool: event.toolName}) + "\n");
    }, { priority: 50 });
  3. Start the gateway (systemctl --user restart openclaw-gateway.service) and confirm register() fires (one log line per gateway start).
  4. Drive a native exec from any agent harness — e.g. ask the agent through Discord to run ls.
  5. Observe: the agent returns the correct directory listing (proving native exec ran end-to-end), but /tmp/btc-trace.log stays empty.

Switching the same handler to after_tool_call reproduces the opposite — the trace log is written on every tool call. The plugin code path itself works; only before_tool_call is silent.

Expected behavior

Per src/agents/pi-tools.before-tool-call.ts, the wrapper at wrapToolWithBeforeToolCallHook (line 597) calls runBeforeToolCallHook (line 396) on every tool execution. That function reads the global hook runner via getGlobalHookRunner() (line 474) and gates further work on hookRunner?.hasHooks("before_tool_call") (line 528). Plugin handlers registered via api.on("before_tool_call", ...) end up in registry.typedHooks (src/plugins/registry.ts:2034), which hasHooks reads lazily (src/plugins/hooks.ts:1344-1345).

So the handler should fire on every wrapped tool call. before_tool_call is not in CONVERSATION_HOOK_NAMES (src/plugins/hook-types.ts), so the allowConversationAccess gate in registerTypedHook (src/plugins/registry.ts:2010) does not apply — non-bundled plugins should be able to register without any policy opt-in.

Actual behavior

The plugin's register() runs (logged once at gateway start). A real exec tool call runs end-to-end (the agent returns correct command output). The plugin's before_tool_call handler is never invoked.

The most likely root cause is at src/agents/pi-tools.before-tool-call.ts:528: the getGlobalHookRunner() call at line 474 either returns null or returns a runner whose registry.typedHooks does not include the plugin's handler in the runtime context the native-exec dispatch path uses. The short-circuit at line 528 then returns { blocked: false } without ever invoking hookRunner.runBeforeToolCall. We have not pinpointed whether this is a runtime-mode issue around activatePluginRegistry / preserveGatewayHookRunner (src/plugins/loader.ts:1411-1425), a singleton-init ordering bug, or something else; but the empirical signal is that before_tool_call is silent while after_tool_call works in the same plugin and same gateway PID.

OpenClaw version

2026.4.29 (a448042)

Operating system

Ubuntu 24.04.4 LTS

Install method

npm global (npm install -g [email protected])

Model

anthropic/claude-opus-4-7

Provider / routing chain

openclaw -> anthropic

Additional provider/model setup details

Single in-process gateway PID (no subagent / external runtime). Anthropic harness via @mariozechner/pi-coding-agent running embedded inside the gateway process. No Codex, no native-hook-relay path involved — confirmed there is exactly one node process in the gateway tree.

Logs, screenshots, and evidence

# Plugin register() — fires once at gateway start
2026-05-02 13:39:21 [skill-channel-gate] register() called; rules=1

# Real exec turn — Discord HITL, agent returned correct ls output
2026-05-02 13:48:25  user: @Juno run ls
2026-05-02 13:48:44  Juno: AGENTS.md  CLAUDE.md  HEARTBEAT.md  IDENTITY.md  NOTES.md  README.md  SOUL.md  TOOLS.md  USER.md  docs/  plugins/  skills/

# Trace handler output — never written (file remained 0 bytes, mtime = pre-test truncate)
$ wc -c /tmp/skill-channel-gate-trace.log
0 /tmp/skill-channel-gate-trace.log

# No "[skill-channel-gate] before_tool_call" line ever appears in journal
$ journalctl --user -u openclaw-gateway --since "13:48" | grep skill-channel-gate
2026-05-02 13:39:21 [skill-channel-gate] register() called; rules=1

Plugin source (10 lines, only the handler shown):

api.on("before_tool_call", (event, ctx) => {
  try {
    appendFileSync("/tmp/skill-channel-gate-trace.log",
      JSON.stringify({ts: Date.now(), pid: process.pid, toolName: event?.toolName, sessionKey: ctx?.sessionKey, paramsKeys: Object.keys(event?.params ?? {})}) + "\n");
    console.log(`[skill-channel-gate] before_tool_call: toolName=${event?.toolName}`);
  } catch (e) { console.error(e); }
  return undefined;
}, { priority: 50 });

Impact and severity

  • Affected: All plugins that need to gate, modify, or audit native tool calls (exec, read, write, etc.) before execution under the Anthropic harness on 2026.4.29 stable.
  • Severity: High. before_tool_call is the documented surface for tool-execution policy plugins; with it silent, plugins cannot enforce per-channel/per-tenant restrictions at the execution boundary, leaving after_tool_call audit-only.
  • Frequency: Always (1/1 register, 1/1 exec turns observed). No intermittency; the handler simply never fires.
  • Consequence: For our use case (a Discord-tenant-scoped agent that needs to block specific skills outside specific channels) this means the execution-surface tenant wall is not enforceable from a plugin; we are reduced to HITL discipline until this is fixed.

Additional information

  • before_tool_call is not in CONVERSATION_HOOK_NAMES so the allowConversationAccess policy gate does not apply.
  • The same plugin's after_tool_call handler does fire on the same exec turns — the plugin code path and registration are reaching the registry; only the dispatch-time read of plugin handlers for before_tool_call is producing zero hits.
  • Local codex review was not run (codex CLI not installed on the repro machine). Happy to add a regression test alongside a fix once a maintainer confirms the diagnosis direction.

extent analysis

TL;DR

The before_tool_call hook is not being invoked due to an issue with the getGlobalHookRunner() call or the registry.typedHooks not including the plugin's handler, and a potential workaround is to investigate and fix the activatePluginRegistry or preserveGatewayHookRunner functionality.

Guidance

  • Investigate the getGlobalHookRunner() call at src/agents/pi-tools.before-tool-call.ts:474 to ensure it returns a valid hook runner with the plugin's handler in its registry.typedHooks.
  • Verify that the activatePluginRegistry and preserveGatewayHookRunner functions in src/plugins/loader.ts:1411-1425 are working correctly and not causing issues with the plugin registry.
  • Check the ordering of singleton initialization to ensure that the plugin registry is properly initialized before the before_tool_call hook is invoked.
  • Consider adding logging or debugging statements to the runBeforeToolCallHook function to understand why the before_tool_call hook is not being invoked.

Example

No code example is provided as the issue is more related to the internal workings of the OpenClaw framework and requires a deeper understanding of the codebase.

Notes

The issue seems to be specific to the OpenClaw framework and the Anthropic harness, and the provided information suggests that the before_tool_call hook is not being invoked due to an issue with the hook runner or plugin registry. Further investigation and debugging are required to determine the root cause and develop a fix.

Recommendation

Apply a workaround by investigating and fixing the activatePluginRegistry or preserveGatewayHookRunner functionality, as the issue seems to be related to the plugin registry and hook runner. This will likely require a deeper understanding of the OpenClaw framework and its internal workings.

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 src/agents/pi-tools.before-tool-call.ts, the wrapper at wrapToolWithBeforeToolCallHook (line 597) calls runBeforeToolCallHook (line 396) on every tool execution. That function reads the global hook runner via getGlobalHookRunner() (line 474) and gates further work on hookRunner?.hasHooks("before_tool_call") (line 528). Plugin handlers registered via api.on("before_tool_call", ...) end up in registry.typedHooks (src/plugins/registry.ts:2034), which hasHooks reads lazily (src/plugins/hooks.ts:1344-1345).

So the handler should fire on every wrapped tool call. before_tool_call is not in CONVERSATION_HOOK_NAMES (src/plugins/hook-types.ts), so the allowConversationAccess gate in registerTypedHook (src/plugins/registry.ts:2010) does not apply — non-bundled plugins should be able to register without any policy opt-in.

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]: Plugin `before_tool_call` hook does not fire for native exec on 2026.4.29 (Anthropic harness) [1 comments, 2 participants]