Skip to content

[Bug]: before_tool_call hook receives undefined sessionKey and agentId for plugin-registered tools #19381

Description

@patosullivan

Summary

Plugin-registered before_tool_call hooks receive undefined for both ctx.sessionKey and ctx.agentId, making per-session access control impossible.

Steps to reproduce

  1. Create a channel plugin that registers a tool and a before_tool_call hook:
const plugin = {
  id: "my-plugin",
  register(api: OpenClawPluginApi) {
    // Register a tool
    api.registerTool({
      name: "my_tool",
      description: "Test tool",
      parameters: { type: "object", properties: {} },
      async execute() {
        return { content: [{ type: "text", text: "ok" }] };
      },
    });

    // Register hook to intercept tool calls
    api.on("before_tool_call", (event, ctx) => {
      api.logger.info(
        `Hook ctx: agentId=${ctx.agentId}, sessionKey=${ctx.sessionKey}, toolName=${ctx.toolName}`
      );
    });
  },
};
  1. Send a message that triggers the agent to call my_tool

  2. Observe logs

Expected behavior

ctx.sessionKey and ctx.agentId should be populated with the current session context:

Hook ctx: agentId=main, sessionKey=agent:main:tlon:dm:~user, toolName=my_tool

Actual behavior

Both fields are undefined:

Hook ctx: agentId=undefined, sessionKey=undefined, toolName=my_tool

OpenClaw version

2026.2.15

Operating system

Docker (node:24-bookworm-slim)

Install method

npm global

Logs, screenshots, and evidence

Impact and severity

This breaks any plugin that needs per-session access control. For example, blocking sensitive tools for certain senders is impossible because the hook cannot identify which session triggered the tool call.

Additional information

Root Cause Analysis

We traced the issue through the bundled code. The bug is in the tool execute wrapper for plugin-registered tools.

Stack Trace

When a plugin tool is called, the call path is:

Object.execute (subagent-registry-kdTa9uwX.js:54212)
  → runBeforeToolCallHook (subagent-registry-kdTa9uwX.js:51735)
    → hookRunner.runBeforeToolCall (deliver-BRNIcAT5.js:153)
      → Plugin hook handler

The Bug

In the bundled code at subagent-registry-kdTa9uwX.js:54212, the tool execute wrapper calls runBeforeToolCallHook without passing ctx:

// Line 54207-54218 (tool execute wrapper)
execute: async (...args) => {
    const { toolCallId, params, onUpdate, signal } = splitToolExecuteArgs(args);
    let executeParams = params;
    try {
        if (!beforeHookWrapped) {
            const hookOutcome = await runBeforeToolCallHook({
                toolName: name,
                params,
                toolCallId
                // ❌ BUG: No `ctx` passed here!
            });
            // ...
        }

But runBeforeToolCallHook expects ctx to extract agentId and sessionKey:

// Line 51735-51743 (runBeforeToolCallHook)
const hookResult = await hookRunner.runBeforeToolCall({
    toolName,
    params: normalizedParams
}, {
    toolName,
    agentId: args.ctx?.agentId,     // ← undefined because args.ctx is undefined
    sessionKey: args.ctx?.sessionKey // ← undefined because args.ctx is undefined
});

Fix

The tool execute wrapper needs to pass the session context when calling runBeforeToolCallHook. The ctx with agentId and sessionKey should be captured in the wrapper closure (similar to how wrapToolWithBeforeToolCallHook does it for core tools).

Additional Context

Core tools work correctly - The before_tool_call hook receives correct sessionKey and agentId when intercepting core tools (like Read, Bash, etc.). This is because core tools are wrapped via wrapToolWithBeforeToolCallHook, which captures and passes the session context.

Plugin-registered tools don't - When a plugin registers its own tool via api.registerTool(), that tool goes through a different code path that doesn't pass the session context to the hook.

The sessionKey exists upstream - The session context is correctly available when core tools are invoked (we can see it in the before_tool_call hook for core tools). The data exists; it's just not being threaded through to the plugin tool execute wrapper.

No workaround available - Because the context is lost in the tool execute wrapper, there's no way for a plugin to correlate a before_tool_call event with the session that triggered it. This makes per-session access control impossible for plugin-registered tools.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingstaleMarked as stale due to inactivity

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions