Skip to content

[Bug]: Plugin before_tool_call hook does not fire for native exec on 2026.4.29 (Anthropic harness) #76201

@juno02139

Description

@juno02139

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 openclaw@2026.4.29).
  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 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions