diff --git a/src/auto-reply/reply/dispatch-from-config.ts b/src/auto-reply/reply/dispatch-from-config.ts index 70616302259..7da65b23654 100644 --- a/src/auto-reply/reply/dispatch-from-config.ts +++ b/src/auto-reply/reply/dispatch-from-config.ts @@ -225,14 +225,11 @@ export async function dispatchReplyFromConfig(params: { toPluginMessageContext(hookContext), ); if (beforeDispatchResult?.block) { - if (beforeDispatchResult.replyText) { - dispatcher.sendFinalReply({ text: beforeDispatchResult.replyText }); - } + const queuedFinal = beforeDispatchResult.replyText + ? dispatcher.sendFinalReply({ text: beforeDispatchResult.replyText }) + : false; recordProcessed("skipped", { reason: "before_dispatch_blocked" }); - return { - queuedFinal: Boolean(beforeDispatchResult.replyText), - counts: dispatcher.getQueuedCounts(), - }; + return { queuedFinal, counts: dispatcher.getQueuedCounts() }; } } diff --git a/src/plugins/hooks.ts b/src/plugins/hooks.ts index 027b027ff4d..896f2e532fc 100644 --- a/src/plugins/hooks.ts +++ b/src/plugins/hooks.ts @@ -439,6 +439,11 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp * Allows plugins to block the entire message dispatch before LLM invocation * and optionally send a reply text directly to the user. * Runs sequentially — first handler that returns { block: true } wins. + * + * **Error semantics:** Like all hooks, errors are caught and logged when + * `catchErrors` is true (the default). This means a throwing handler + * results in permit-by-default (fail-open). Security-critical plugins + * should handle errors internally to implement fail-closed behavior. */ async function runBeforeDispatch( event: PluginHookBeforeDispatchEvent,