← Back to PRs

#20067: feat(plugins): add before_agent_reply hook for message interception

by JoshuaLelon open 2026-02-18 13:34 View on GitHub →
size: M
## 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