TypeError: hook["event"] is not a function — event hook called as Object on every Bus event #9154

Open
opened 2026-02-16 18:11:46 -05:00 by yindo · 0 comments
Owner

Originally created by @GeoloeG-IsT on GitHub (Feb 12, 2026).

Originally assigned to: @rekram1-node on GitHub.

Bug Report

Environment

  • OpenCode version: 1.1.60 (installed via curl -fsSL https://opencode.ai/install | bash)
  • OS: Linux 6.6.87.2-microsoft-standard-WSL2 x86_64
  • Bun: 1.3.8
  • @opencode-ai/plugin: 1.1.60

Error Message

TypeError: hook["event"] is not a function. (In 'hook["event"]?.({ event: input })', 'hook["event"]' is an instance of Object)

This fires on every Bus event (e.g., file.edited), not just once. The error is non-blocking — edits succeed — but it spams the logs.

Plugin Code

The plugin is a local file at .opencode/plugins/are-check-update.js, auto-discovered by OpenCode. It follows the exact pattern from the official plugin docs:

import { existsSync, mkdirSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { spawn } from 'child_process';

export const AreCheckUpdate = async (ctx) => {
  return {
    event: async ({ event }) => {
      if (event.type !== 'session.created') return;
      // ... spawns background update check ...
    },
  };
};
  • Single named export, no export default
  • Returns { event: async (...) => {...} } which conforms to the Hooks interface
  • No other plugins are installed (only the built-in ones: CodexAuth, CopilotAuth, AnthropicAuth, GitlabAuth)

Analysis

The error originates from Plugin.init() in packages/opencode/src/plugin/index.ts:

Bus.subscribeAll(async (input) => {
  const hooks = await state().then((x) => x.hooks);
  for (const hook of hooks) {
    hook["event"]?.({
      event: input,
    });
  }
});

Optional chaining ?.() only guards against undefined/null. If hook["event"] exists but is an Object (not a function), it throws TypeError.

I verified the plugin works correctly in standalone Bun:

$ bun -e "
const mod = await import('file:///path/to/.opencode/plugins/are-check-update.js');
const result = await mod.AreCheckUpdate({});
console.log(typeof result.event);        // 'function'
console.log(result.event.constructor.name); // 'AsyncFunction'
result.event({ event: { type: 'file.edited' } }); // no error
"

None of the built-in plugins (CodexAuth, CopilotAuth, AnthropicAuth, GitlabAuth) have an event property in their returned hooks — they only use auth, chat.headers, and experimental.chat.system.transform. So our plugin is the only source of an event key in the hooks array.

Relationship to #11392

This appears related to #11392 (fn3 is not a function, fn3 is an instance of Array), which was a similar type confusion in the plugin hook execution path. That issue involved export default causing duplicate entries. Our plugin does NOT use export default, but the error pattern is the same — a hook property that should be a function is somehow resolved as a non-callable Object at runtime inside the compiled binary.

Expected Behavior

hook["event"]?.({ event: input }) should call the async function without error, or at minimum not throw on every Bus event.

Suggested Fix

Add a typeof guard in Plugin.init():

Bus.subscribeAll(async (input) => {
  const hooks = await state().then((x) => x.hooks);
  for (const hook of hooks) {
    const fn = hook["event"];
    if (typeof fn === "function") {
      fn({ event: input });
    }
  }
});

This would prevent the TypeError regardless of how the property gets resolved.

Originally created by @GeoloeG-IsT on GitHub (Feb 12, 2026). Originally assigned to: @rekram1-node on GitHub. ## Bug Report ### Environment - **OpenCode version**: 1.1.60 (installed via `curl -fsSL https://opencode.ai/install | bash`) - **OS**: Linux 6.6.87.2-microsoft-standard-WSL2 x86_64 - **Bun**: 1.3.8 - **`@opencode-ai/plugin`**: 1.1.60 ### Error Message ``` TypeError: hook["event"] is not a function. (In 'hook["event"]?.({ event: input })', 'hook["event"]' is an instance of Object) ``` This fires on every Bus event (e.g., `file.edited`), not just once. The error is non-blocking — edits succeed — but it spams the logs. ### Plugin Code The plugin is a local file at `.opencode/plugins/are-check-update.js`, auto-discovered by OpenCode. It follows the exact pattern from the [official plugin docs](https://opencode.ai/docs/plugins#send-notifications): ```javascript import { existsSync, mkdirSync } from 'fs'; import { homedir } from 'os'; import { join } from 'path'; import { spawn } from 'child_process'; export const AreCheckUpdate = async (ctx) => { return { event: async ({ event }) => { if (event.type !== 'session.created') return; // ... spawns background update check ... }, }; }; ``` - Single named export, **no** `export default` - Returns `{ event: async (...) => {...} }` which conforms to the `Hooks` interface - No other plugins are installed (only the built-in ones: CodexAuth, CopilotAuth, AnthropicAuth, GitlabAuth) ### Analysis The error originates from `Plugin.init()` in `packages/opencode/src/plugin/index.ts`: ```typescript Bus.subscribeAll(async (input) => { const hooks = await state().then((x) => x.hooks); for (const hook of hooks) { hook["event"]?.({ event: input, }); } }); ``` Optional chaining `?.()` only guards against `undefined`/`null`. If `hook["event"]` exists but is an Object (not a function), it throws `TypeError`. I verified the plugin works correctly in standalone Bun: ``` $ bun -e " const mod = await import('file:///path/to/.opencode/plugins/are-check-update.js'); const result = await mod.AreCheckUpdate({}); console.log(typeof result.event); // 'function' console.log(result.event.constructor.name); // 'AsyncFunction' result.event({ event: { type: 'file.edited' } }); // no error " ``` None of the built-in plugins (CodexAuth, CopilotAuth, AnthropicAuth, GitlabAuth) have an `event` property in their returned hooks — they only use `auth`, `chat.headers`, and `experimental.chat.system.transform`. So our plugin is the only source of an `event` key in the hooks array. ### Relationship to #11392 This appears related to #11392 (`fn3 is not a function, fn3 is an instance of Array`), which was a similar type confusion in the plugin hook execution path. That issue involved `export default` causing duplicate entries. Our plugin does NOT use `export default`, but the error pattern is the same — a hook property that should be a function is somehow resolved as a non-callable Object at runtime inside the compiled binary. ### Expected Behavior `hook["event"]?.({ event: input })` should call the async function without error, or at minimum not throw on every Bus event. ### Suggested Fix Add a `typeof` guard in `Plugin.init()`: ```typescript Bus.subscribeAll(async (input) => { const hooks = await state().then((x) => x.hooks); for (const hook of hooks) { const fn = hook["event"]; if (typeof fn === "function") { fn({ event: input }); } } }); ``` This would prevent the TypeError regardless of how the property gets resolved.
yindo added the windows label 2026-02-16 18:11:46 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#9154