Skip to content

before_tool_call hook fires twice: tools double-wrapped after normalizeToolParameters strips the wrap marker #92973

Description

@dertbv

OpenClaw 2026.6.6, macOS launchd install.

Symptom

A plugin that registers a single before_tool_call hook sees its handler execute twice for a subset (~4%) of tool calls, clustered around isolated-agent / cron invocations. In our case ClawLens logs two decision audit entries ~25–36ms apart and emits its own audit double-write warning. The hook is registered exactly once (api.on("before_tool_call", handler, { priority: 100 })), so this is not double-registration.

Root cause (traced through the dist bundle)

The before_tool_call hook is applied to tools via wrapToolWithBeforeToolCallHook. Most call sites guard against re-wrapping with isToolWrappedWithBeforeToolCallHook:

  • native-hook-relay-*.js — guarded ✓
  • openclaw-tools-*.js — guarded ✓
  • agent-tools-*.js (the ~:1242 site) — guarded ✓

But the createOpenClawCodingTools path does not:

// agent-tools-*.js  (createOpenClawCodingTools)
const normalized      = subagentFiltered.map((tool) => normalizeToolParameters(tool, {...}));
const withHooks       = normalized.map((tool) => wrapToolWithBeforeToolCallHook(tool, {...}));  // <-- no isToolWrappedWithBeforeToolCallHook guard

Worse, even adding a guard here would not be sufficient, because normalizeToolParameters constructs a fresh tool via object spread:

return { ...tool, ...addEmptyObjectArgumentPreparation(tool, parameters), parameters };

The wrap marker is a Symbol (BEFORE_TOOL_CALL_WRAPPED), and object spread does not copy Symbol-keyed properties. So normalization strips the marker off an already-wrapped tool, after which the unconditional wrapToolWithBeforeToolCallHook re-wraps it. The result is nested wrappers — each wrappers executecallsrunBeforeToolCallHookbefore delegating to the inner (also-wrapped)execute` — so the hook runs twice per invocation.

This fires for cron / isolated-agent spawns because those rebuild their tool set through createOpenClawCodingToolsnormalizeToolParameterswrapToolWithBeforeToolCallHook, picking up tools that were already wrapped in a prior context.

Impact

  • before_tool_call plugin hooks run twice → duplicate side effects (audit rows, risk eval, alerts, anything a guard plugin does per decision).
  • Deterministic for affected tools; intermittent in aggregate because it depends on which tools carry a prior wrap.

Suggested fix (maintainers to choose placement)

Either:

  1. Make normalizeToolParameters preserve the BEFORE_TOOL_CALL_WRAPPED Symbol marker (and any other wrap markers), or
  2. Guard the createOpenClawCodingTools wrap site with isToolWrappedWithBeforeToolCallHook — but only works if the marker survives normalization, so (1) is likely required regardless.

A defensive isToolWrappedWithBeforeToolCallHook check inside wrapToolWithBeforeToolCallHook itself would be robust against all call sites, but is also defeated by the marker being stripped during normalization — so the marker-preservation fix (1) is the core one.

Happy to test a fix against this install.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:otherThis issue has meaningful maintainer-visible impact outside the owned taxonomy.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.no-staleExclude from stale automation

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions