#9906: feat: wire message_sending hook in outbound delivery
stale
Cluster:
Message Sending Hooks Enhancement
## Summary
Connects the existing `message_sending` plugin hook to the outbound delivery path, allowing plugins to modify outgoing message content before it is sent to channels.
## Motivation
The `message_sending` hook was defined in the plugin API with full type definitions and a runner implementation (`runMessageSending` in `src/plugins/hooks.ts`), but was never called from the delivery pipeline. The sibling `message_received` hook IS wired (in `dispatch-from-config.ts`); this PR completes the pair.
This enables use cases like:
- **Output transformation** (e.g. decoding encoded tokens before delivery)
- **Content moderation / redaction**
- **Logging / auditing outbound messages**
## Changes
- **`src/infra/outbound/deliver.ts`** — Call `runMessageSending` hook on each normalized payload before chunking. Only content modification is supported (returning `{ content: "..." }`). Hook runs pre-chunk intentionally so transformations apply to the full text before channel-specific splitting.
- **`src/infra/outbound/deliver.ts`** — Mirror transcript now uses post-hook text so it reflects what was actually delivered, not the pre-hook original.
- **`src/infra/outbound/deliver.test.ts`** — Two new tests: content modification via hook, and verification that hook is skipped when no handlers are registered.
## Design Decisions
**Content-modification only (no cancel):** The hook supports `{ content: "..." }` returns but intentionally does not support cancellation. Cancel semantics in mixed-payload batches (text + media) create complex edge cases around mirror transcripts and partial delivery. Content modification covers the primary use cases.
**Pre-chunk placement is intentional:** The hook runs after `normalizeReplyPayloadsForDelivery()` but before `sendTextChunks()`. This means plugins see the full text and transformations (e.g. decoding tokens) apply before the text is split into channel-sized messages. Chunks are then based on the final delivered content length.
**Mirror reflects delivered content:** The mirror transcript is built from `normalizedPayloads` after hook processing, so session transcripts match what was actually sent to the channel.
## Backwards Compatibility
Non-breaking. Hook only runs if plugins register handlers for `message_sending`. No existing behavior changes when no hooks are registered.
## Previous Review
This addresses feedback from #9889 (closed):
- ✅ Removed `cancel` support to avoid mirror/chunking edge cases
- ✅ Mirror now uses post-hook text
- ✅ Added comment clarifying pre-chunk placement is intentional
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
This PR wires the existing `message_sending` plugin hook into the outbound delivery pipeline (`src/infra/outbound/deliver.ts`) so plugins can modify outgoing text before channel chunking. It also updates mirror transcript generation to reflect the post-hook text that was actually delivered, and adds unit tests covering the hook modification path and the “no handlers registered” fast-path.
<h3>Confidence Score: 4/5</h3>
- Mostly safe to merge once hook semantics are clarified.
- Change is localized and test-covered, but the delivery path currently ignores the `cancel` field that `runMessageSending` can produce, which risks plugins believing they can cancel outbound messages when they cannot.
- src/infra/outbound/deliver.ts
<!-- greptile_other_comments_section -->
<sub>(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#8084: fix(plugins): wire up message_sending hook in outbound delivery
by lailoo · 2026-02-03
91.1%
#12584: feat(hooks): wire outbound message lifecycle hooks
by vincentkoc · 2026-02-09
86.0%
#20859: fix(hooks): wire message_sent hook into reply dispatcher for all ch...
by davidrudduck · 2026-02-19
81.9%
#10109: feat(plugins): invoke message_received and message_sent hooks
by nezovskii · 2026-02-06
81.7%
#11597: feat(hooks): implement message:received hook
by gnufoo · 2026-02-08
80.3%
#19922: feat(hooks): add message:received and message:sent hook events
by NOVA-Openclaw · 2026-02-18
79.8%
#7545: feat(hooks): add message:received hook for pre-turn automation
by wangtian24 · 2026-02-02
79.5%
#7771: Hooks: wire lifecycle events and tests
by rabsef-bicrym · 2026-02-03
79.3%
#11681: feat(plugins): add cancel support to message_received hook
by PrimeTenet · 2026-02-08
78.7%
#15577: feat(hooks): add message:preprocessed hook event
by heybeaux · 2026-02-13
78.5%