#20067: feat(plugins): add before_agent_reply hook for message interception
size: M
Cluster:
Plugin and Hook Enhancements
## Summary
- Adds a new `before_agent_reply` plugin hook that fires after slash commands/directives are handled but before the LLM agent runs
- Plugins can return a synthetic `ReplyPayload` to short-circuit agent processing, enabling forms, wizards, approval flows, and interactive tutorials as plugins — without modifying core
- Follows existing hook patterns (`runModifyingHook`, sequential by priority, first reply wins)
## Motivation
Per [VISION.md](https://github.com/openclaw/openclaw/blob/main/VISION.md): *"Core stays lean; optional capability should usually ship as plugins."*
Currently there's no way for plugins to intercept inbound messages and return a synthetic reply before the LLM runs. Features like dialog wizards, approval gates, and interactive tutorials must modify core code. This hook fills that gap.
Partially addresses #13004 (Feature Request: Message interception hooks).
### Relationship to existing PRs
Several open PRs attempt related message-interception (#18004, #11681, #10539, #15577, #12082, #7091, #14544). This hook is differentiated by firing at the right pipeline position — after commands/directives (so `/help` always works, even during a plugin dialog) — and supporting full `ReplyPayload` short-circuit (not just cancel/block).
## Design
**Hook name:** `before_agent_reply`
**When it fires:** After `handleInlineActions` returns `kind: "continue"`, before `stageSandboxMedia` / `runPreparedReply` (~line 287 of `get-reply.ts`).
**Event type:**
```ts
{ cleanedBody: string } // final user message heading to LLM
```
**Result type:**
```ts
{ reply?: ReplyPayload; reason?: string }
```
**Execution model:** Async, sequential by priority (highest first). First plugin to return a `reply` wins. Uses `runModifyingHook` — same pattern as `before_tool_call` and `before_model_resolve`.
## Changes
| File | Lines | What |
|---|---|---|
| `src/plugins/types.ts` | +18 | Hook name, event/result types, handler map entry |
| `src/plugins/hooks.ts` | +30 | Merge function, `runBeforeAgentReply`, re-exports, return object |
| `src/auto-reply/reply/get-reply.ts` | +13 | Hook call site (import + 7-line check) |
| `src/plugins/hooks.before-agent-reply.test.ts` | +162 | 9 unit tests |
**Total: ~61 lines of production code, ~162 lines of tests.**
## What this does NOT include
- No new gateway methods or protocol changes
- No session schema changes
- No dialog/form/wizard functionality — just the hook infrastructure
- No plugin implementation
## Test plan
- [x] `pnpm build` — compiles
- [x] `pnpm check` — lint + format pass (0 warnings, 0 errors)
- [x] `pnpm test src/plugins/hooks.before-agent-reply.test.ts` — 9/9 pass
- [x] `pnpm test src/plugins/` — all 115 existing plugin tests unaffected
- [x] `pnpm test src/auto-reply/` — all 720 existing reply tests unaffected
### Test coverage
1. Hook returns `reply` → result contains reply
2. Hook returns empty object → result passes through (no reply)
3. No hooks registered → result is undefined
4. Multiple hooks → highest priority's `reply` wins
5. Lower-priority reply ignored when higher-priority provided one
6. Lower-priority can provide reply when higher-priority returns nothing
7. Hook throws → error caught (`catchErrors: true`), result is undefined
8. `hasHooks` returns true/false correctly
## AI Disclosure 🤖
- [x] **AI-assisted:** This PR was authored with Claude Code (Claude Opus 4.6)
- [x] **Degree of testing:** Fully tested — `pnpm build && pnpm check` pass, 9 new unit tests, 115 existing plugin tests and 720 existing auto-reply tests verified unaffected
- [x] **I understand what the code does:** Adds a `before_agent_reply` entry to the plugin hook system's type union, handler map, and hook runner, then wires a ~7-line call site in `get-reply.ts` that checks registered plugins after inline actions are handled and short-circuits to the plugin's `ReplyPayload` if one is returned
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Adds a new `before_agent_reply` plugin hook that fires after slash commands/directives are processed but before the LLM agent runs, allowing plugins to return a synthetic `ReplyPayload` to short-circuit agent processing. This enables plugin-driven flows like wizards, approval gates, and interactive tutorials without modifying core.
- **Hook implementation** in `hooks.ts` follows the established `runModifyingHook` pattern with proper priority ordering and merge semantics (`acc?.reply ?? next.reply`), consistent with `before_tool_call` and `before_model_resolve`
- **Call site** in `get-reply.ts` is correctly positioned after `handleInlineActions` returns `kind: "continue"` and before `stageSandboxMedia`/`runPreparedReply`, with a `hasHooks` guard for performance — matching existing hook call site patterns (e.g., `before_reset` in `commands-core.ts`)
- **Types** are clean and well-structured; the `PluginHookHandlerMap` entry correctly supports sync and async handlers
- **Test coverage** is thorough with 9 unit tests covering priority ordering, error handling, empty results, and `hasHooks`
- Minor style nit: `before_agent_reply` is placed between `gateway_start` and `gateway_stop` in the `PluginHookName` union rather than with the other agent hooks (`before_agent_start`, `llm_input`, etc.)
<h3>Confidence Score: 4/5</h3>
- This PR is safe to merge — it adds a well-tested, low-risk hook following established patterns with no changes to existing behavior.
- The implementation is clean, follows existing hook patterns precisely, and has thorough test coverage. The only findings are style-level (hook name ordering in the union type). No logic bugs, no security concerns, and no risk of breaking existing functionality.
- No files require special attention. All changes follow established patterns.
<sub>Last reviewed commit: 43e7b5c</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#18004: feat: Add before_message_dispatch hook for blocking inbound messages
by Andy-Haigh · 2026-02-16
83.7%
#22624: feat(plugins): add before_context_send hook and model routing via b...
by davidrudduck · 2026-02-21
82.3%
#14544: feat: add before_context_send plugin hook
by Windelly · 2026-02-12
81.6%
#11124: feat(plugins): add before_llm_request hook for custom LLM headers
by johnlanni · 2026-02-07
81.6%
#11732: feat(plugins): add injectMessages to before_agent_start hook
by antra-tess · 2026-02-08
80.7%
#8022: feat: implement before_model_select plugin hook
by dead-pool-aka-wilson · 2026-02-03
77.8%
#23559: feat(plugins): add before_context_send hook and model routing via b...
by davidrudduck · 2026-02-22
77.8%
#16618: feat: bridge message lifecycle hooks to workspace hook system
by DarlingtonDeveloper · 2026-02-14
77.6%
#20426: feat: make llm_input/llm_output modifying hooks for middleware patt...
by chandika · 2026-02-18
77.3%
#16028: feat/before-tool-result
by ambushalgorithm · 2026-02-14
77.2%