Skip to content

bug(opencode): plugin hooks session.created and tui.prompt.append never fire — wrong API #418

Description

@paolomainardi

Bug: opencode plugin hooks never fire — wrong hook names

Summary

The opencode plugin (src/plugins/opencode/plugin.js) registers session.created and tui.prompt.append as named hook keys, but opencode's plugin API does not support these as trigger-able hooks. The flag file is never written and the per-turn reinforcement never fires.

Root cause

opencode's plugin system (@opencode-ai/plugin) defines a Hooks interface with specific named hooks that can be triggered via plugin.trigger(name, input, output). The supported hooks are:

  • event (catch-all event bus listener)
  • chat.message
  • chat.params
  • chat.headers
  • command.execute.before
  • tool.execute.before / tool.execute.after
  • shell.env
  • experimental.chat.system.transform
  • experimental.chat.messages.transform
  • experimental.session.compacting
  • experimental.compaction.autocontinue

session.created and tui.prompt.append are not in this list. They exist as bus events (for internal UI use) but are never dispatched to plugins via the trigger() mechanism.

Additionally, opencode's trigger pattern is (input, output) => Promise<void> with mutation on output — return values are discarded. The current plugin returns { append: ... } which is silently ignored.

Evidence

  • Plugin loads successfully (confirmed in logs: service=plugin path=file:///...plugin.js loading plugin)
  • session.created bus event is published but never dispatched to plugin hooks
  • tui.prompt.append is a TUI input-field event (inserts text into prompt widget), not a pre-send hook
  • Tested with opencode 1.15.4 on Linux

Fix

The correct hooks to use:

  1. Session init: Run in the plugin factory function (which executes once per session at load time)
  2. User prompt interception: Use chat.message hook (input, output) => Promise<void>output.parts contains text parts with the user's message
  3. System prompt injection: Use experimental.chat.system.transform hook (input, output) => Promise<void> — push reinforcement line to output.system array

Patch

export const CavemanPlugin = async (_ctx) => {
  // Initialize flag at plugin load time (equivalent to session start).
  const mode = getDefaultMode();
  if (mode === 'off') {
    try { if (existsSync(flagPath)) unlinkSync(flagPath); } catch (e) {}
  } else {
    safeWriteFlag(flagPath, mode);
  }

  return {
    'chat.message': async (_input, output) => {
      if (!output || !output.parts) return;
      for (const part of output.parts) {
        if (part && part.type === 'text' && part.text) {
          const change = parseModeChange(part.text);
          if (change) applyModeChange(change);
        }
      }
    },

    'experimental.chat.system.transform': async (_input, output) => {
      const active = readFlag(flagPath);
      if (active && !INDEPENDENT_MODES.has(active)) {
        output.system.push(reinforcementLine(active));
      }
    },
  };
};

Tested with

  • opencode 1.15.4
  • opencode run headless mode
  • Verified: flag init ✓, /caveman ultra ✓, stop caveman ✓, system prompt injection ✓

Platform

  • OpenCode
  • Claude Code
  • Codex
  • Other

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions