Skip to content

[FEATURE]: Support blocking/cancelling user messages in chat.message plugin hook #30434

@itdove

Description

@itdove

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

The chat.message plugin hook fires before the message is sent to the LLM (confirmed by reading packages/opencode/src/session/prompt.ts line ~1075). Plugins can mutate output.parts in-place to modify the message content. However, there is no documented or supported way to block/cancel a message entirely.

Currently:

  • throw new Error() — stops plugin execution but the message still goes through (the Effect promise rejects but OpenCode silently catches it)
  • return { noReply: true } — return value is ignored by the trigger() function
  • Mutating output.parts — works as a workaround to replace message content, but the original message is still "sent" (just with replaced content)

Proposed solution

Add an official mechanism to cancel a user message from chat.message hooks. Options:

  1. Output flag: Allow output.cancel = true or output.blocked = true to prevent the message from being sent to the LLM. The UI should display a notification explaining the block.
  2. Throw-to-block: Make throw new Error("reason") in chat.message actually cancel the message and display the error to the user (similar to how tool.execute.before throw blocks tool execution).
  3. Return value: Check the return value of chat.message hooks — { cancel: true, reason: "..." } cancels the message.

Use case

Security plugins need to scan user prompts for secrets, PII, and prompt injection attacks before they reach the LLM. Without a proper blocking mechanism:

  • Secrets pasted in prompts reach the model even if detected
  • The only workaround is mutating output.parts to replace content — this works but the user gets no clear UI feedback that their message was blocked (the LLM just receives a synthetic message and responds accordingly)

This is equivalent to Claude Code's UserPromptSubmit hook which can return { decision: "block", reason: "..." } to prevent a prompt from being sent.

Current workaround

Mutate output.parts in-place to replace the user's message with a security warning:

async 'chat.message'(input, output) {
  const text = output.parts
    .filter(p => p.type === 'text' && !p.synthetic)
    .map(p => p.text || '').join('\n');
  
  if (containsSecret(text)) {
    const firstPart = output.parts[0] || {};
    output.parts.length = 0;
    output.parts.push({
      ...firstPart,
      type: 'text',
      text: 'Message blocked: secret detected.',
      synthetic: true,
    });
  }
}

This works because output.parts is the same reference as resolvedParts in prompt.ts, so mutations propagate. But it's fragile and undocumented.

Related issues

Additional context

  • ai-guardian uses this workaround successfully for OpenCode integration
  • The trigger() function in packages/opencode/src/plugin/index.ts calls hooks sequentially and returns output — it could check for a cancel flag before returning

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions