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
- 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}`
);
});
},
};
-
Send a message that triggers the agent to call my_tool
-
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.
Summary
Plugin-registered
before_tool_callhooks receiveundefinedfor bothctx.sessionKeyandctx.agentId, making per-session access control impossible.Steps to reproduce
before_tool_callhook:Send a message that triggers the agent to call
my_toolObserve logs
Expected behavior
ctx.sessionKeyandctx.agentIdshould be populated with the current session context:Actual behavior
Both fields are
undefined: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:
The Bug
In the bundled code at
subagent-registry-kdTa9uwX.js:54212, the tool execute wrapper callsrunBeforeToolCallHookwithout passingctx:But
runBeforeToolCallHookexpectsctxto extractagentIdandsessionKey:Fix
The tool execute wrapper needs to pass the session context when calling
runBeforeToolCallHook. ThectxwithagentIdandsessionKeyshould be captured in the wrapper closure (similar to howwrapToolWithBeforeToolCallHookdoes it for core tools).Additional Context
Core tools work correctly - The
before_tool_callhook receives correctsessionKeyandagentIdwhen intercepting core tools (likeRead,Bash, etc.). This is because core tools are wrapped viawrapToolWithBeforeToolCallHook, 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_callhook 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_callevent with the session that triggered it. This makes per-session access control impossible for plugin-registered tools.