Skip to content

Plugin hook \ ool.execute.after\ is declared but never triggered #25918

@luismichio

Description

@luismichio

Summary

The tool.execute.after hook is declared in the Hooks interface in @opencode-ai/plugin but is never invoked anywhere in the OpenCode runtime. Any plugin implementing this hook will have its handler registered but silently ignored — the handler is never called.

Details

Hook declaration

The hook is correctly typed in packages/plugin/src/index.ts:

"tool.execute.after"?: (
  input: { tool: string; sessionID: string; callID: string; args: any },
  output: {
    title: string
    output: string
    metadata: any
  },
) => Promise<void>

The plugin.trigger mechanism in packages/opencode/src/plugin/index.ts is also correctly implemented — it passes output by reference, calls all registered handlers, and returns the (possibly mutated) output object:

const trigger = Effect.fn("Plugin.trigger")(function* (name, input, output) {
  if (!name) return output
  const s = yield* InstanceState.get(state)
  for (const hook of s.hooks) {
    const fn = hook[name] as any
    if (!fn) continue
    yield* Effect.promise(async () => fn(input, output))
  }
  return output
})

Missing call site

A search through the entire OpenCode server codebase found zero call sites for plugin.trigger("tool.execute.after", ...):

  • packages/opencode/src/session/processor.ts — handles tool-result stream events and calls completeToolCall(), but no hook trigger
  • packages/opencode/src/session/llm.ts — assembles AI SDK tools and handles streaming, no hook trigger
  • packages/opencode/src/tool/registry.ts — only triggers tool.definition
  • packages/opencode/src/session/session.ts — no plugin references at all
  • packages/opencode/src/agent/agent.ts — only triggers experimental.chat.system.transform
  • All v2 session files — no hook trigger

Where the trigger should logically live

In packages/opencode/src/session/processor.ts, the tool-result event handler at around line 380 receives the completed tool output and calls completeToolCall(). This is the natural location to trigger tool.execute.after:

case "tool-result": {
  // ... EventV2 dual-write ...

  // MISSING: yield* plugin.trigger("tool.execute.after", { ... }, value.output)

  yield* completeToolCall(value.toolCallId, value.output)
  return
}

The value.output object has the shape { title, output, metadata } which matches the hook's declared output parameter exactly.

Impact

Plugins that implement tool.execute.after to intercept or modify tool output (e.g., to inject context, compress output, or add telemetry) have no effect. The hook infrastructure is in place but the wire-up is missing.

Proposed Fix

In packages/opencode/src/session/processor.ts, add the plugin trigger before completeToolCall:

case "tool-result": {
  const toolCall = yield* readToolCall(value.toolCallId)
  // ... existing EventV2 dual-write ...

  // Trigger the after hook so plugins can observe/modify tool output
  const hookInput = {
    tool: value.toolName,
    sessionID: ctx.sessionID,
    callID: value.toolCallId,
    args: toolCall?.part.state.status === "running" ? toolCall.part.state.input : {},
  }
  const hookOutput = yield* plugin.trigger("tool.execute.after", hookInput, {
    title: value.output.title,
    output: value.output.output,
    metadata: value.output.metadata,
  })

  yield* completeToolCall(value.toolCallId, { ...value.output, ...hookOutput })
  return
}

Similarly, tool.execute.before (also declared but never triggered) should be wired up at the tool-call event site.

Also affected

  • tool.execute.before — declared in Hooks but also never triggered in the runtime.

Environment

  • OpenCode version: 1.14.39
  • Found via: source audit of packages/opencode/src/ on commit history at time of investigation

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