Repository navigation
Description
Problem
Plugins authoring chat.message hook can mutate the user message and parts, but they cannot signal "I handled this turn out-of-band, please skip the assistant generation". The hook always falls through to the LLM call.
This blocks plugin-only implementations of side-question features (/btw-style commands) where the plugin synthesizes the answer itself and does not want a real assistant turn after that. Today the parent assistant still runs once on the mutated user message, which produces an extra unwanted reply.
Existing pieces that already do most of this
PromptInput.noReplyalready exists and short-circuitsloop():
opencode/packages/opencode/src/session/prompt.ts
Line 1387 in aa3c99a
if (input.noReply === true) return message experimental.compaction.autocontinueis the same shape: a hook output flag (enabled) read back by the caller to skip work:
opencode/packages/opencode/src/session/compaction.ts
Lines 511 to 527 in aa3c99a
(yield* plugin.trigger( "experimental.compaction.autocontinue", { sessionID: input.sessionID, agent: userMessage.agent, model: yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID), provider: { source: info.source, info, options: info.options, }, message: userMessage, overflow: input.overflow === true, }, { enabled: true }, )).enabled ) { plugin.trigger()already returns the mutated output object, so reading it back fromchat.messageis a no-op infra-wise:
opencode/packages/opencode/src/plugin/index.ts
Lines 258 to 271 in aa3c99a
const trigger = Effect.fn("Plugin.trigger")(function* < Name extends TriggerName, Input = Parameters<Required<Hooks>[Name]>[0], Output = Parameters<Required<Hooks>[Name]>[1], >(name: Name, input: Input, output: Output) { if (!name) return output const s = yield* InstanceState.get(state) for (const hook of s.hooks) { const fn = hook[name] as any if (!fn) continue yield* Effect.promise(async () => fn(input, output)) } return output })
Proposed change (minimal)
Add an optional noReply?: boolean to the chat.message hook output, and have createUserMessage/prompt honor it.
// packages/plugin/src/index.ts
"chat.message"?: (
input: { sessionID, agent?, model?, messageID?, variant? },
output: {
message: UserMessage
parts: Part[]
/** If true, skip the assistant turn after this user message is persisted. */
noReply?: boolean
},
) => Promise<void>// packages/opencode/src/session/prompt.ts createUserMessage
const triggered = yield* plugin.trigger("chat.message", { ... }, { message: info, parts, noReply: false as boolean })
// ...
return { info, parts, noReply: triggered.noReply === true }// packages/opencode/src/session/prompt.ts prompt()
if (input.noReply === true || message.noReply === true) return messageThat is the whole change. No new hook event, no new abort API, reuses PromptInput.noReply semantics that already exist.
Use case
Building a /btw-style side-question slash command in a plugin. The plugin runs the question in an isolated child session, writes the answer back into the parent user message via chat.message, and currently has no way to tell the parent session "we are done, do not run the assistant on this turn".
Related
- [FEATURE]: add /btw command #16992
/btwfeature request (resolved by plugin author tradition; this would unblock cleaner plugin implementations) - feat: Add noReply option to command.execute.before hook #9306 same
noReplyrequest forcommand.execute.before(closed for inactivity; same pattern wanted here for a different hook) - [FEATURE]: Add cancellation support for
command.execute.beforehook #18554 cancellation support forcommand.execute.before(open; same underlying gap, sibling hook) - Plugins using noReply seem to cause model to switch to agent default #4475 mentions
noReplyalready existing inPromptInput
Happy to send a PR if maintainers are open to this. Wanted to file the issue first per CONTRIBUTING.md.
Activity
github-actions commentedon Jul 8, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.