Skip to content

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

@GeoloeG-IsT

Description

@GeoloeG-IsT

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.

Metadata

Metadata

Assignees

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