openclaw - 💡(How to fix) Fix Plugin SDK: MessagePreSent outbound hook missing [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#72539Fetched 2026-04-28 06:34:48
View on GitHub
Comments
0
Participants
1
Timeline
0
Reactions
0

The OpenClaw plugin SDK (v2026.4.x) exposes a rich hook system for inbound message processing (MessageReceived, MessagePreprocessed, MessageTranscribed) and post-send observation (MessageSent), but does not expose a pre-send hook that would allow plugins to inspect, validate, modify, or reject outbound messages before they are dispatched to the channel adapter.

Multiple in-flight architecture atoms across the OpenClaw deployment require pre-send filtering capability: pre-action validation on approval-button taps, HITL pre-send content review, outbound content-policy validators (cost limits, recipient sanity checks, attachment scanning), and fabrication safeguard mechanisms (validating that LLM-emitted artefact-state claims match disk-state before send). Without a pre-send hook event, these atoms must either modify each send-script directly (per-channel per-script, fragile) or implement send-wrappers (behavioural discipline, can be routed around).

Proposed fix: add MessagePreSent hook event type to the plugin SDK + plumb through the gateway send pipeline to channel adapters, with handler return value capable of allowing/rejecting/modifying the outbound payload.

Error Message

| MessageSent | Post-send — has success: boolean + error?: string fields, fires AFTER the send attempt | YES | MessageSent analysed: it is unambiguously post-send. The hook context type definition (internal-hooks.d.ts:MessageSentHookContext) includes success: boolean and error?: string fields — the hook fires after the send completes (or fails). Useful for logging/auditing/observation but cannot prevent or modify a message. 4. On adapter dispatch completion, fire existing MessageSent hook with success + error? as today.

Root Cause

The OpenClaw plugin SDK (v2026.4.x) exposes a rich hook system for inbound message processing (MessageReceived, MessagePreprocessed, MessageTranscribed) and post-send observation (MessageSent), but does not expose a pre-send hook that would allow plugins to inspect, validate, modify, or reject outbound messages before they are dispatched to the channel adapter.

Multiple in-flight architecture atoms across the OpenClaw deployment require pre-send filtering capability: pre-action validation on approval-button taps, HITL pre-send content review, outbound content-policy validators (cost limits, recipient sanity checks, attachment scanning), and fabrication safeguard mechanisms (validating that LLM-emitted artefact-state claims match disk-state before send). Without a pre-send hook event, these atoms must either modify each send-script directly (per-channel per-script, fragile) or implement send-wrappers (behavioural discipline, can be routed around).

Proposed fix: add MessagePreSent hook event type to the plugin SDK + plumb through the gateway send pipeline to channel adapters, with handler return value capable of allowing/rejecting/modifying the outbound payload.

Fix Action

Fix / Workaround

Severity: HIGH — load-bearing for autonomous-unpause critical path; sole structural-floor pathway available given workspace-side substitute (Sub-W Stage I) indefinitely deferred 2026-04-27 per substrate-construction blockers (Sub-A1 implementation, postman Telegram-delivery extension); current workarounds are behavioural rather than structural Upstream project: OpenClaw plugin SDK + gateway send pipeline

The OpenClaw plugin SDK (v2026.4.x) exposes a rich hook system for inbound message processing (MessageReceived, MessagePreprocessed, MessageTranscribed) and post-send observation (MessageSent), but does not expose a pre-send hook that would allow plugins to inspect, validate, modify, or reject outbound messages before they are dispatched to the channel adapter.

Hook event typePhaseAvailable in SDK?
MessageReceivedInbound — message arrives at gatewayYES
MessagePreprocessedInbound — enrichment (transcripts, redaction, etc.) before agent sees itYES
MessageTranscribedInbound — audio→text transcription completionYES
MessageSentPost-send — has success: boolean + error?: string fields, fires AFTER the send attemptYES
MessagePreSent / MessageOutbound / BeforeSendPre-send — would intercept outbound payload before dispatchNOT FOUND

Code Example

grep -rn "preSend\|beforeSend\|pre.send\|outbound.*hook" \
  ~/.nvm/versions/node/v22.22.1/lib/node_modules/openclaw/dist/plugin-sdk/src \
  --include="*.d.ts"

---

export type MessagePreSentHookContext = {
  /** Recipient identifier */
  to: string;
  /** Message text content */
  content: string;
  /** Channel identifier (e.g., "telegram", "fastmail-jmap") */
  channelId: string;
  /** Provider account ID for multi-account setups */
  accountId?: string;
  /** Conversation/chat ID */
  conversationId?: string;
  /** Reply-to message ID, if applicable */
  replyToId?: string | null;
  /** Thread ID for forum-style channels */
  threadId?: string | number | null;
  /** Attachments (files to be sent alongside message) */
  attachments?: Array<{ path: string; mimeType?: string; size?: number }>;
  /** Whether this message would be sent in a group/channel context */
  isGroup?: boolean;
  /** Group or channel identifier, if applicable */
  groupId?: string;
  /** Identity claim from sender */
  identity?: { id: string; type: string };
  /** Additional channel-specific metadata */
  metadata?: Record<string, unknown>;
};

export type MessagePreSentHookHandlerResult =
  | { action: "allow" }
  | { action: "reject"; reason: string; surface?: "log" | "governance" }
  | { action: "modify"; payload: Partial<MessagePreSentHookContext> };

export type MessagePreSentHookEvent = InternalHookEvent & {
  type: "message";
  action: "pre-sent";
  context: MessagePreSentHookContext;
};

---

events: ["message:pre-sent"]
RAW_BUFFERClick to expand / collapse

Plugin SDK: MessagePreSent outbound hook missing

This issue is being filed as part of a batch of related upstream submissions discovered during architectural review of OpenClaw deployment. Filed locally 2026-04-25; submitted upstream 2026-04-27. See "Related issues (batch)" section at end for batch context.

Suggested labels (Mike to choose from labels actually available in target repo at submission time): bug, severity:high, plugin-sdk, hooks, gateway

Severity: HIGH — load-bearing for autonomous-unpause critical path; sole structural-floor pathway available given workspace-side substitute (Sub-W Stage I) indefinitely deferred 2026-04-27 per substrate-construction blockers (Sub-A1 implementation, postman Telegram-delivery extension); current workarounds are behavioural rather than structural Upstream project: OpenClaw plugin SDK + gateway send pipeline

Summary

The OpenClaw plugin SDK (v2026.4.x) exposes a rich hook system for inbound message processing (MessageReceived, MessagePreprocessed, MessageTranscribed) and post-send observation (MessageSent), but does not expose a pre-send hook that would allow plugins to inspect, validate, modify, or reject outbound messages before they are dispatched to the channel adapter.

Multiple in-flight architecture atoms across the OpenClaw deployment require pre-send filtering capability: pre-action validation on approval-button taps, HITL pre-send content review, outbound content-policy validators (cost limits, recipient sanity checks, attachment scanning), and fabrication safeguard mechanisms (validating that LLM-emitted artefact-state claims match disk-state before send). Without a pre-send hook event, these atoms must either modify each send-script directly (per-channel per-script, fragile) or implement send-wrappers (behavioural discipline, can be routed around).

Proposed fix: add MessagePreSent hook event type to the plugin SDK + plumb through the gateway send pipeline to channel adapters, with handler return value capable of allowing/rejecting/modifying the outbound payload.

Environment

  • Host: DESKTOP-CB8PBCM (Ubuntu 24.04, WSL2 on Windows 11 Pro)
  • OpenClaw version: 2026.4.x (gateway bound to loopback, standard deployment)
  • Plugin SDK version: as bundled with OpenClaw 2026.4.x at ~/.nvm/versions/node/v22.22.1/lib/node_modules/openclaw/dist/plugin-sdk/

Spike findings (2026-04-25 12:18 AEST)

Files inspected:

  1. ~/.nvm/versions/node/v22.22.1/lib/node_modules/openclaw/dist/plugin-sdk/src/hooks/internal-hooks.d.ts (full file).
  2. ~/.nvm/versions/node/v22.22.1/lib/node_modules/openclaw/dist/plugin-sdk/src/hooks/types.d.ts (full file).
  3. ~/.nvm/versions/node/v22.22.1/lib/node_modules/openclaw/dist/plugin-sdk/src/channels/plugins/outbound.types.d.ts (head 50 lines).
  4. ~/.nvm/versions/node/v22.22.1/lib/node_modules/openclaw/dist/plugin-sdk/src/cli/outbound-send-mapping.d.ts (full file).
  5. skills/fastmail-jmap/scripts/send.js in workspace (full file, 720 lines).

Hook events documented in plugin SDK:

Hook event typePhaseAvailable in SDK?
MessageReceivedInbound — message arrives at gatewayYES
MessagePreprocessedInbound — enrichment (transcripts, redaction, etc.) before agent sees itYES
MessageTranscribedInbound — audio→text transcription completionYES
MessageSentPost-send — has success: boolean + error?: string fields, fires AFTER the send attemptYES
MessagePreSent / MessageOutbound / BeforeSendPre-send — would intercept outbound payload before dispatchNOT FOUND

Search methodology:

grep -rn "preSend\|beforeSend\|pre.send\|outbound.*hook" \
  ~/.nvm/versions/node/v22.22.1/lib/node_modules/openclaw/dist/plugin-sdk/src \
  --include="*.d.ts"

Result: zero matches across the plugin-sdk .d.ts definition files.

MessageSent analysed: it is unambiguously post-send. The hook context type definition (internal-hooks.d.ts:MessageSentHookContext) includes success: boolean and error?: string fields — the hook fires after the send completes (or fails). Useful for logging/auditing/observation but cannot prevent or modify a message.

Outbound channel adapter context exists (channels/plugins/outbound.types.d.ts:ChannelOutboundContext): the gateway constructs a ChannelOutboundContext with to, text, mediaUrl, replyToId, etc. and passes it to the channel adapter for dispatch. This context is not exposed to a hook event — it goes directly from gateway send-pipeline to adapter.

Why this is a gateway/plugin-SDK gap, not workspace defect

The send pipeline architecturally supports the moment we need: between gateway-receives-send-request and adapter-dispatches-to-provider, there is a ChannelOutboundContext materialisation step. A MessagePreSent hook event fired at this step — with handler return capable of { allow }, { reject, reason }, or { modify, payload } semantics — would unblock multiple downstream atoms.

Currently the only ways to intercept outbound are:

  • (a) Modify each send-script directly (e.g. skills/fastmail-jmap/scripts/send.js for email; per-channel scripts for Telegram, etc.). Per-channel + per-script + fragile against future SDK upgrades.
  • (b) Wrap each send invocation in a workspace-controlled wrapper script. Behavioural — agents must invoke the wrapper rather than direct send tool. Can be routed around by direct invocation.
  • (c) Build the hook event into core. Right answer architecturally, but requires upstream cooperation.

Currently in production: (b) is in use via scripts/tmp/send-*.sh patterns (see workspace recent commits, e.g. F7-A scanner Gate 2 audit brief send). Behavioural workaround. Not enforceable.

Use cases blocked by absent MessagePreSent hook

(a) Approval-button-tap pre-action-validation

TASK-GOVERNANCE-INLINE-APPROVAL (in tasks/tasks.json, P2, pending) wires Telegram inline buttons (Approve / Approve Always / Reject) onto Governance APPROVAL posts. On button-tap, callback handler writes decision to state/audit/approvals/AUDIT-ID.json. The follow-on outbound message ("AUTO-APPROVED" / "APPROVED" Governance post confirmation) is currently a normal outbound — no pre-send validation that the approval-decision-file exists, that the user_id matches Mike's ID, that the callback is fresh.

A MessagePreSent hook would let the approval-button atom validate the full evidence chain before the confirmation message ships.

(b) HITL pre-send content review

docs/hitl-escalation-spec.md Tier-3 risk class (load-bearing actions) currently relies on Alfred-to-Mike confirmation chat before action. There is no structural enforcement — Alfred could send an action-trigger message directly. A MessagePreSent hook on outbound action-triggers would enforce HITL gate.

(c) Outbound content-policy validators

  • Recipient sanity (does the address match approved-recipients list per Fastmail config — currently checked in send.js but only there).
  • Attachment scanning (cost-attached files, sensitive-data leakage).
  • Cost limits (per-message, per-day Anthropic spend caps).
  • Per-tier recipient routing (Tier 4 messages to Governance only, Tier 1 to DMs allowed, etc.).

All currently behavioural-discipline. Centralised pre-send hook would consolidate.

(d) Fabrication safeguard Sub-B-FAR

TASK-FAB-B-PRE-SEND-HOOK-BUILD (prose queue, P2) needs this hook to validate that any artefact-state claim in an outbound message (e.g. "spec is 550 lines") matches the actual disk state of the cited artefact at send-time. Without the hook, validation can only happen via send-wrapper discipline (Sub-A2 in current architecture), which is behavioural-with-mechanical-augmentation rather than structural enforcement.

This is the load-bearing use case for autonomous-mode unpause — the Stage 2 safeguard distinction in docs/autonomous-infrastructure-architecture.md (post-update). Without MessagePreSent, autonomous-mode at 8-hour stretch has no structural fabrication-prevention surface.

2026-04-27 ~07:30 AEST update — operational context revised under §3 REV 2 §3.7 Option γ trigger:

Workspace-side substitute (Sub-W Stage I W1+W2+W3 mechanisms per §4 Option I) was deferred indefinitely 2026-04-27 per substrate-construction blockers: Sub-A1 marker schema implementation has not shipped; postman trigger-Telegram-delivery substrate does not exist as production code path. §6 REV 2 §6.5 Condition H — the recommended pre-Sub-B-FAR readiness condition — presupposes §4 Option I full ship (line 198); Condition H's coverage projection (events #2, #3, W2/W3-covered subsets of #4 bounded by mechanism cadence) is undercut by deferral. Pre-Sub-B-FAR detection surface for events #1-#4 is now Mike-attention exclusively.

Sub-B-FAR is the only structural-floor mechanism available in the near-term timeline. Until Sub-A1 ships AND postman extension lands AND Sub-W deploys (estimated weeks-to-month), Sub-B-FAR remains the sole pathway from Mike-attention-only detection to mechanism-driven detection on output-side fabrication-shape events.

Submission timing advances to pre-Foundation Gate 4 per §3 REV 2 §3.6 line 236 ("Recommendation is Option γ. The workspace-side substitute would not cover any of the interim period; the timing question is dominated by Sub-B-FAR submission alone"). Severity escalation reflects this operational reality.

Proposed fix

Add MessagePreSent hook event to the plugin SDK with the following shape:

Hook event type (in internal-hooks.d.ts)

export type MessagePreSentHookContext = {
  /** Recipient identifier */
  to: string;
  /** Message text content */
  content: string;
  /** Channel identifier (e.g., "telegram", "fastmail-jmap") */
  channelId: string;
  /** Provider account ID for multi-account setups */
  accountId?: string;
  /** Conversation/chat ID */
  conversationId?: string;
  /** Reply-to message ID, if applicable */
  replyToId?: string | null;
  /** Thread ID for forum-style channels */
  threadId?: string | number | null;
  /** Attachments (files to be sent alongside message) */
  attachments?: Array<{ path: string; mimeType?: string; size?: number }>;
  /** Whether this message would be sent in a group/channel context */
  isGroup?: boolean;
  /** Group or channel identifier, if applicable */
  groupId?: string;
  /** Identity claim from sender */
  identity?: { id: string; type: string };
  /** Additional channel-specific metadata */
  metadata?: Record<string, unknown>;
};

export type MessagePreSentHookHandlerResult =
  | { action: "allow" }
  | { action: "reject"; reason: string; surface?: "log" | "governance" }
  | { action: "modify"; payload: Partial<MessagePreSentHookContext> };

export type MessagePreSentHookEvent = InternalHookEvent & {
  type: "message";
  action: "pre-sent";
  context: MessagePreSentHookContext;
};

Plumbing into gateway send pipeline

In the gateway's outbound dispatch path (where ChannelOutboundContext is constructed and passed to the channel adapter), insert a hook-firing step:

  1. Materialise MessagePreSentHookContext from ChannelOutboundContext.
  2. Fire hook handlers registered for events: ["message:pre-sent"].
  3. Aggregate handler results:
    • All allow → proceed to channel adapter.
    • Any reject → abort send, write structured log entry, optionally surface to Governance.
    • Any modify → apply modifications (payload-merge), recurse to step 2 with modified context (capped at N=3 to prevent loops).
  4. On adapter dispatch completion, fire existing MessageSent hook with success + error? as today.

Registration

Plugins registering a MessagePreSent handler use the existing OpenClawHookMetadata.events array:

events: ["message:pre-sent"]

Backward compatibility

Existing plugins (using MessageReceived, MessageSent, MessagePreprocessed) unaffected. Adding MessagePreSent is additive — no breaking changes to current handler signatures or send-pipeline behaviour for plugins that don't register for it.

Performance contract

Hook-firing adds latency to every outbound message. Suggested implementation: hook handlers run sequentially (not parallel) but with per-handler timeout (default 500ms, configurable). Handlers exceeding timeout receive default { action: "allow" } with logged warning. Total pre-send overhead bounded at (N_handlers × 500ms) worst case; typical deployments with ≤3 handlers see <100ms additional latency.

Invariant to preserve

If no plugins register for message:pre-sent, the gateway send pipeline behaves identically to current. Zero-handler case must be a no-op (no log entry, no measurable latency). Adoption is opt-in per plugin.

Workaround status

Currently deployed: send-wrappers in workspace (scripts/tmp/send-*.sh pattern) that invoke skills/fastmail-jmap/scripts/send.js after pre-validation. Sub-A2 of the fabrication-safeguards atom will formalise this pattern with claim-vs-disk-state validation at the wrapper layer.

This workaround is:

  • Effective for workspace-controlled outbound (Alfred follows the wrapper discipline).
  • Per-channel (each channel needs its own wrapper).
  • Behavioural (Alfred can route around by invoking send.js directly; gateway doesn't enforce).
  • Reversible: when the gateway fix lands, wrapper logic moves into a plugin registered for message:pre-sent.

Request for upstream review

Please consider:

  1. Reviewing the proposed fix shape for fit with OpenClaw's plugin SDK design conventions.
  2. Confirming whether modify action semantics (handler returns modified payload) is desirable or whether allow|reject only is preferred (simpler invariant).
  3. Advising whether the timeout default (500ms per handler) is appropriate given current channel adapter performance budgets.
  4. Flagging any other current workaround patterns in the OpenClaw codebase that would be obsoleted by this hook (we can document the workaround inventory if useful).
  5. Whether this should be an additive hook event, or whether MessageSent should be extended to support a phase: "pre" | "post" discriminator (alternate API shape — would unify pre/post into one event with phase tag).

Data available on request

  • Full spike output (4 plugin-SDK .d.ts files inspected, search methodology + zero-match result).
  • Workspace send-wrapper inventory (scripts/tmp/send-*.sh patterns currently in use).
  • Use case briefs for the 4 blocked atoms (TASK-GOVERNANCE-INLINE-APPROVAL, HITL Tier-3, content-policy validators, TASK-FAB-B-PRE-SEND-HOOK-BUILD).
  • OpenClaw version + gateway config.

Related issues (batch)

This issue is filed as part of a batch of related upstream submissions:

  • [anthropics/claude-code#53710] F12-A: Glob tool false-negative on recently-created files — Anthropic Claude Code repo
  • [#72541] F11-C: Gateway completeness check false-negative on pure-relay agents — OpenClaw repo
  • [#72540] Opus 4.7: supportsAdaptiveThinking allowlist missing claude-opus-4-7 — OpenClaw repo
  • [#72539] Sub-B-FAR: Plugin SDK MessagePreSent outbound hook missing — OpenClaw repo

extent analysis

TL;DR

Add a MessagePreSent hook event to the OpenClaw plugin SDK to allow plugins to inspect, validate, modify, or reject outbound messages before they are dispatched to the channel adapter.

Guidance

  • Review the proposed MessagePreSentHookContext and MessagePreSentHookHandlerResult types to ensure they meet the requirements of the plugin SDK and gateway send pipeline.
  • Implement the hook-firing step in the gateway's outbound dispatch path, materializing MessagePreSentHookContext from ChannelOutboundContext and firing hook handlers registered for events: ["message:pre-sent"].
  • Ensure backward compatibility by making the MessagePreSent hook additive, with no breaking changes to current handler signatures or send-pipeline behavior for plugins that don't register for it.
  • Consider the performance contract and invariant to preserve, ensuring that the hook-firing adds minimal latency and that the zero-handler case is a no-op.

Example

export type MessagePreSentHookContext = {
  to: string;
  content: string;
  channelId: string;
  // ...
};

export type MessagePreSentHookHandlerResult =
  | { action: "allow" }
  | { action: "reject"; reason: string; surface?: "log" | "governance" }
  | { action: "modify"; payload: Partial<MessagePreSentHookContext> };

Notes

The proposed fix assumes that the MessagePreSent hook is desirable and that the modify action semantics are acceptable. It's essential to review the design conventions and performance budgets of the OpenClaw plugin SDK and gateway send pipeline to ensure a seamless integration.

Recommendation

Apply the proposed workaround by adding the MessagePreSent hook event to the plugin SDK, as it provides a structural enforcement mechanism for pre-send validation and modification of outbound messages, addressing the load-bearing use cases blocked by the absent hook

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix Plugin SDK: MessagePreSent outbound hook missing [1 participants]