#Is there a plugin hook that intercepts ALL outgoing messages before they're sent to a channel?

1 messages · Page 1 of 1 (latest)

woven pebble
#

We built a plugin (@andrewlabs/openclaw-messageguard-ml) that registers a message_sending hook via api.on("message_sending", ...) to scan and redact secrets in outgoing messages. The plugin loads fine and registers successfully, but the hook never fires — not for agent replies, not for message tool sends.

Specifically:

openclaw plugins list shows the plugin as loaded
The plugin logs "registered message_sending hook" at startup
But sending messages (both via the message tool and normal agent replies) passes through unfiltered — no hook invocation

What we're looking for: A hook that acts as outgoing middleware — intercepts the message content before it hits the channel (Telegram, Signal, etc.), lets us modify or cancel/mask it, regardless of whether it originated from an agent reply, the message tool, or any other send path.

Is message_sending the right hook for this? If so, what are we missing? If not, what's the correct approach?

Running OpenClaw 2026.2.23 on Linux.

vivid rapidsBOT
#

To help others find answers, you can mark your question as solved via Right click solution message -> Apps -> âś… Mark Solution

warped orioleBOT
#

message_sending is the intended “outgoing middleware” hook (it’s the only plugin hook that can modify content or cancel a send via { content, cancel }). You’re not crazy — the reason you never see it fire is that not all outbound send paths currently go through the hook runner.

Why it doesn’t fire (what you’re missing)

In 2026.2.23, a lot of real-world outbound messages are sent via channel-specific code paths that bypass the core outbound delivery pipeline where message_sending is invoked.

Two common bypasses that match your symptoms:

  1. Agent replies for many channels (Discord, etc.)
    Those are delivered by the channel monitor/reply dispatcher (e.g. Discord’s deliverDiscordReply(...)) which calls the provider send functions directly and does not run plugin hooks beforehand.

  2. message tool sends
    The message tool often routes through channels.<id>.actions.handleAction (channel “message actions”). Those handlers can send directly as well, and today they don’t invoke message_sending either—so tool sends also slip past.

So: message_sending exists, but it’s not wired as a universal egress middleware yet.

What you can do today

  • For tool-driven sends (the message tool): use the before_tool_call hook and redact/block when event.toolName === "message" and event.params.action === "send" (or "broadcast"). That will reliably intercept tool sends because it runs before tool execution.
#
  • For agent “normal replies”: there isn’t currently a supported hook that lets you rewrite/cancel the final assistant reply right before it’s delivered across all channels.

What’s the “correct approach” long-term

This needs a core change: OpenClaw should ensure every outbound send path (channel reply dispatchers + channel message-actions + core delivery) funnels through a single outbound boundary that runs:

  • message_sending (modifiable/cancellable)
  • then the provider send
  • then message_sent

If you want this to be real “intercept ALL outbound”, it’s a feature request / bug. Best next step is to open a GitHub issue with:

  • “message_sending not fired for Discord agent replies / message tool sends”
  • the channels you tested
  • minimal repro steps

Issue tracker: https://github.com/openclaw/openclaw/issues

If you paste:

#
  • openclaw plugins info @andrewlabs/openclaw-messageguard-ml --json
  • and which channel(s) you tested (Discord/Telegram/Signal)
    …I can tell you which exact send path you’re hitting and whether before_tool_call will cover the tool case for your setup.
woven pebble
#

Thanks krill, we did this:

Two fixes:

1. Hook: before_tool_call instead of message_sending

message_sending never fires (confirmed by Krill — not wired up for tool sends or most channel replies in 2026.2.23). We switched to before_tool_call which intercepts the message tool before execution:

api.on("before_tool_call", async (event, ctx) => {
    if (event.toolName !== "message") return;
    if (params?.action !== "send" && params?.action !== "broadcast") return;
    // ... detect & redact, return { params: { ...params, message: redacted } }
});

We kept message_sending registered too, so when OpenClaw eventually wires it up properly, that path will also work.

2. Classifier: span reconstruction was broken

The model output was fine (high confidence, correct classifications). But start/end offsets are undefined in transformers.js for this model, so the code fell back to indexOf per subword token — which matched fragments at wrong positions (##7 matching a random 7 elsewhere).

Fix: group consecutive sensitive subword tokens into word groups (split on non-## tokens), reconstruct the full text fragment, find it case-insensitively in the original, then extend to the next word boundary to catch trailing characters the tokenizer dropped.

warped orioleBOT
#

Yep — that’s the right workaround on 2026.2.23.

1) before_tool_call is the only reliable intercept for message tool right now

A small nit in your snippet: you probably meant event.params, not params:

api.on("before_tool_call", async (event, ctx) => {
if (event.toolName !== "message") return;

const params = event.params ?? {};
if (params.action !== "send" && params.action !== "broadcast") return;

const redacted = redact(String(params.message ?? params.text ?? params.content ?? ""));

return {
params: {
#
...params,
// cover the common aliases so you don’t miss a schema variant
message: redacted,
text: redacted,
content: redacted,
caption: redacted,
},
};
});

(And yeah, keeping message_sending registered “for the future” is smart — it is designed to be the universal outbound middleware, it’s just not consistently on every egress path yet.)

2) Span reconstruction: your fix is the right direction, but watch 2 edge cases

Your “group ## subwords → reconstruct fragment → find in original → extend to boundary” approach is exactly what you end up doing when the tokenizer won’t give offsets. Two gotchas to harden it: