Skip to content

OpenCode hooks never fire in the OpenCode Desktop app: plugin uses Bun globals, desktop runs the server on Node #2014

Description

@Soph

What happened?

Entire's hooks never fire when OpenCode is used through the OpenCode Desktop app (Mac). The CLI/TUI works. No error surfaces anywhere — not in the app, not in .entire/logs/ — because the failure is swallowed.

Root cause: the desktop app runs the OpenCode server on Node (inside an Electron utility process), not Bun, and our plugin calls the Bun global directly.

cmd/entire/cli/agent/opencode/entire_plugin.ts spawns every hook through Bun:

  • Bun.spawn — line 37 (callHook)
  • Bun.spawnSync — line 58 (callHookSync)
  • Bun.spawnSync — line 95 (fireTurnStart)
  • Bun.spawn — line 143 (session-start)

Under Node each call throws ReferenceError: Bun is not defined before any process is spawned, and every call site is wrapped in catch {} ("plugin failures must not crash OpenCode"). So the plugin loads, registers, receives events, and fires zero hooks, silently: no session-start, no turn-start (so no context injection), no turn-end (so no checkpoints), no session-end.

Why the runtime differs

The desktop app does not shell out to the opencode CLI. It forks an Electron utility process and imports a Node build of the server into it (opencode repo, v1.18.18):

  • packages/desktop/src/main/index.ts:378 → spawnLocalServer(...) — the default path (the alternate "v2 CLI daemon" path is gated behind OPENCODE_SIDECAR_V2=1, index.ts:64,333)
  • packages/desktop/src/main/sidecar.ts:57 → await import("virtual:opencode-server")
  • resolves to packages/opencode/dist/node/node.js (electron.vite.config.ts:6,68), built by script/build-node.ts with target: "node"

OpenCode handles its own side of this by nulling out the shell helper it hands plugins:

// packages/opencode/src/plugin/index.ts:164-165
// @ts-expect-error
$: typeof Bun === "undefined" ? undefined : Bun.$,

That guard landed in opencode 535343bf56 ("refactor(server): replace Bun serve with Hono node adapters"), the commit that created the node build for the desktop app. Note the typed plugin API still declares $: BunShell as non-optional (packages/plugin/src/index.ts:65), so this is a general trap for OpenCode plugins, not just ours.

Everything else on the path is fine: plugin discovery is the same code in both runtimes (.opencode/plugins/*.ts globbed at packages/opencode/src/config/plugin.ts:22), the desktop probes the user's login shell for PATH before forking the sidecar (packages/desktop/src/main/server.ts:44-55, inherited via createSidecarEnv()), and no plugin-disabling flags are set.

Steps to reproduce

  1. entire enable --agent opencode in a repo (writes .opencode/plugins/entire.ts).
  2. Open that repo in the OpenCode Desktop app, run a prompt that edits a file, then commit.
  3. No checkpoint is created and the commit gets no Entire-Checkpoint trailer. The same flow in opencode (TUI or opencode run) works.

Isolated repro of the mechanism (our plugin's exact hook shape, run under each runtime):

--- node ---  HOOK SWALLOWED ERROR: ReferenceError Bun is not defined
--- bun  ---  HOOK OK turn-end

Fix

Replace the four Bun.spawn/Bun.spawnSync calls with node:child_process spawn/spawnSync. That module works identically in both runtimes — verified:

--- node ---  result: HOOK RAN turn-end stdin={"session_id":"x"} status: 0
--- bun  ---  result: HOOK RAN turn-end stdin={"session_id":"x"} status: 0

spawnSync keeps the blocking semantics turn-start/turn-end depend on (state ready before mid-turn commits; completion before opencode run tears down its event loop). The file's only other Bun-ism is the import type line, which is erased at load. The header comment "Requires Bun runtime" should go too.

Test coverage: the plugin currently has no test that exercises it under Node. A canary that loads entire_plugin.ts with node and asserts a hook actually spawns would have caught this.

Follow-ups found while diagnosing (separate from the fix above)

  1. .ts plugin extension is a latent hazard. Plugins are loaded with a bare await import(fileURL) (packages/opencode/src/plugin/loader.ts:135) and OpenCode registers no TS loader. Bun imports .ts natively; Node only does since type stripping became default (22.18+/24+). Verified the import succeeds on Node 26; Electron 42's exact Node build is unverified. The discovery glob accepts {ts,js}, so writing the file as .js removes the dependency on runtime TS support entirely. If it does fail there the symptom differs — OpenCode publishes a visible Failed to load plugin session error (plugin/index.ts:191-213) — so a user seeing a toast means this, and silence means the Bun issue above.

  2. The desktop sidecar is one long-lived server shared across all sessions, unlike one CLI process per run. resetSessionTracking (line 109) assumes a single active session at a time, so two sessions open in the same project will thrash the store, fire spurious session-start, and mis-attribute turn-start. And server.instance.disposed (line 247), our only reliable session-end trigger, now fires at app quit rather than per session.

  3. Storage skew to check when validating. The app overrides XDG_STATE_HOME to Electron's userData (packages/desktop/src/main/server.ts:52), but the session DB lives under XDG_DATA_HOME (packages/core/src/database/database.ts:53), which it leaves alone — so opencode export <id> from our hook shares the CLI's DB. However the DB filename is channel-scoped (opencode-<channel>.db for anything not latest/beta/prod, database.ts:54) and the desktop's channel is stamped at build time, defaulting to dev (electron.vite.config.ts:8-13). A dev/nightly app build paired with a stable CLI means different DB files, so opencode export finds nothing even once hooks fire.

Environment

  • Entire CLI 0.10.1-nightly.202608130635.9862b0dd6
  • macOS, Darwin 25.6.0 arm64
  • Agent: OpenCode — Desktop app (@opencode-ai/desktop 1.18.18, Electron 42.3.3)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

agent-supportadding support for additional AI agentsbugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions