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
- Install OpenClaw 2026.4.29 globally (
npm install -g openclaw@2026.4.29).
- 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 });
- Start the gateway (
systemctl --user restart openclaw-gateway.service) and confirm register() fires (one log line per gateway start).
- Drive a native exec from any agent harness — e.g. ask the agent through Discord to run
ls.
- 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 openclaw@2026.4.29)
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.
Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
A plugin that registers a
before_tool_callhook never sees that handler invoked when a native tool (e.g.exec) runs through the Anthropic harness on2026.4.29(commita448042). The plugin'sregister()runs, the same plugin'safter_tool_callhandler does fire, butbefore_tool_callis silently skipped — so plugins cannot block or modify native tool calls before execution.Steps to reproduce
npm install -g openclaw@2026.4.29).plugins/dir whoseregister()only does:systemctl --user restart openclaw-gateway.service) and confirmregister()fires (one log line per gateway start).ls.execran end-to-end), but/tmp/btc-trace.logstays empty.Switching the same handler to
after_tool_callreproduces the opposite — the trace log is written on every tool call. The plugin code path itself works; onlybefore_tool_callis silent.Expected behavior
Per
src/agents/pi-tools.before-tool-call.ts, the wrapper atwrapToolWithBeforeToolCallHook(line 597) callsrunBeforeToolCallHook(line 396) on every tool execution. That function reads the global hook runner viagetGlobalHookRunner()(line 474) and gates further work onhookRunner?.hasHooks("before_tool_call")(line 528). Plugin handlers registered viaapi.on("before_tool_call", ...)end up inregistry.typedHooks(src/plugins/registry.ts:2034), whichhasHooksreads lazily (src/plugins/hooks.ts:1344-1345).So the handler should fire on every wrapped tool call.
before_tool_callis not inCONVERSATION_HOOK_NAMES(src/plugins/hook-types.ts), so theallowConversationAccessgate inregisterTypedHook(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 realexectool call runs end-to-end (the agent returns correct command output). The plugin'sbefore_tool_callhandler is never invoked.The most likely root cause is at
src/agents/pi-tools.before-tool-call.ts:528: thegetGlobalHookRunner()call at line 474 either returnsnullor returns a runner whoseregistry.typedHooksdoes 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 invokinghookRunner.runBeforeToolCall. We have not pinpointed whether this is a runtime-mode issue aroundactivatePluginRegistry/preserveGatewayHookRunner(src/plugins/loader.ts:1411-1425), a singleton-init ordering bug, or something else; but the empirical signal is thatbefore_tool_callis silent whileafter_tool_callworks 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 openclaw@2026.4.29)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-agentrunning embedded inside the gateway process. No Codex, no native-hook-relay path involved — confirmed there is exactly onenodeprocess in the gateway tree.Logs, screenshots, and evidence
Plugin source (10 lines, only the handler shown):
Impact and severity
exec,read,write, etc.) before execution under the Anthropic harness on 2026.4.29 stable.before_tool_callis the documented surface for tool-execution policy plugins; with it silent, plugins cannot enforce per-channel/per-tenant restrictions at the execution boundary, leavingafter_tool_callaudit-only.Additional information
before_tool_callis not inCONVERSATION_HOOK_NAMESso theallowConversationAccesspolicy gate does not apply.after_tool_callhandler 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 forbefore_tool_callis producing zero hits.codex reviewwas 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.