tool.execute.after hook mutations to output.output are not reflected in the UI #9316

Closed
opened 2026-02-16 18:12:10 -05:00 by yindo · 2 comments
Owner

Originally created by @isanchez31 on GitHub (Feb 14, 2026).

Originally assigned to: @thdxr on GitHub.

Summary

Mutations made to output.output inside a tool.execute.after plugin hook are silently ignored. The UI never displays the modified output because OpenCode reads from result.content (an array of content parts) instead of result.output (the string the hook exposes).

This makes it impossible for plugins to annotate or modify tool output after execution, despite the hook signature suggesting mutation is supported.

Steps to Reproduce

  1. Create a plugin that mutates output.output in tool.execute.after:
"tool.execute.after": async (input, output) => {
  if (input.tool !== "bash") return

  const text = output.output ?? ""
  if (text.includes("Operation not permitted")) {
    output.output = text + "\n\n⚠️ [my-plugin] Command was blocked."
  }
}
  1. Run a bash command that triggers the condition (e.g., a sandboxed command that outputs "Operation not permitted")

  2. Observe that the annotation (⚠️ [my-plugin] Command was blocked.) does not appear in the UI output.

Root Cause

In packages/opencode/src/session/prompt.ts, the tool.execute.after hook receives the result object by reference:

const result = await item.execute(args, ctx)

await Plugin.trigger(
  "tool.execute.after",
  { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId, args },
  result,
)

The hook can mutate result.output (a string). However, the code that builds the UI output reads result.content (an array), not result.output:

const textParts: string[] = []
for (const contentItem of result.content) {
  if (contentItem.type === "text") {
    textParts.push(contentItem.text) // ← this is what gets displayed
  }
}

Since result.output is never read after the hook, any mutations to it are discarded.

Hook Type Signature (from @opencode-ai/plugin)

"tool.execute.after"?: (
  input: { tool: string; sessionID: string; callID: string; args: any },
  output: {
    title: string;
    output: string;   // ← plugins can mutate this, but it's never used
    metadata: any;
  },
) => Promise<void>;

The type signature exposes output.output as mutable, but result.content (which is what actually drives the UI) is not part of the hook's output type.

Expected Behavior

One of:

  1. Propagate mutations: After the hook returns, sync result.output back into result.content so that mutations are reflected in the UI.
  2. Expose content: Add result.content to the hook's output type so plugins can directly modify what gets displayed.
  3. Document as read-only: If mutation is intentionally unsupported, update the plugin documentation to clarify that tool.execute.after is observation-only and cannot modify output.

Context

Discovered while building opencode-sandbox (npm), a plugin that sandboxes agent bash commands using @anthropic-ai/sandbox-runtime. The plugin attempts to annotate blocked commands in the output but the annotations never appear.

Environment

  • OpenCode: v1.2.1
  • OS: Ubuntu 24.04 (Linux 6.8.0)
  • Plugin: opencode-sandbox@0.1.1
Originally created by @isanchez31 on GitHub (Feb 14, 2026). Originally assigned to: @thdxr on GitHub. ## Summary Mutations made to `output.output` inside a `tool.execute.after` plugin hook are silently ignored. The UI never displays the modified output because OpenCode reads from `result.content` (an array of content parts) instead of `result.output` (the string the hook exposes). This makes it impossible for plugins to annotate or modify tool output after execution, despite the hook signature suggesting mutation is supported. ## Steps to Reproduce 1. Create a plugin that mutates `output.output` in `tool.execute.after`: ```typescript "tool.execute.after": async (input, output) => { if (input.tool !== "bash") return const text = output.output ?? "" if (text.includes("Operation not permitted")) { output.output = text + "\n\n⚠️ [my-plugin] Command was blocked." } } ``` 2. Run a bash command that triggers the condition (e.g., a sandboxed command that outputs `"Operation not permitted"`) 3. Observe that the annotation (`⚠️ [my-plugin] Command was blocked.`) does **not** appear in the UI output. ## Root Cause In [`packages/opencode/src/session/prompt.ts`](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/prompt.ts), the `tool.execute.after` hook receives the `result` object by reference: ```typescript const result = await item.execute(args, ctx) await Plugin.trigger( "tool.execute.after", { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId, args }, result, ) ``` The hook can mutate `result.output` (a string). However, the code that builds the UI output reads `result.content` (an array), not `result.output`: ```typescript const textParts: string[] = [] for (const contentItem of result.content) { if (contentItem.type === "text") { textParts.push(contentItem.text) // ← this is what gets displayed } } ``` Since `result.output` is never read after the hook, any mutations to it are discarded. ## Hook Type Signature (from `@opencode-ai/plugin`) ```typescript "tool.execute.after"?: ( input: { tool: string; sessionID: string; callID: string; args: any }, output: { title: string; output: string; // ← plugins can mutate this, but it's never used metadata: any; }, ) => Promise<void>; ``` The type signature exposes `output.output` as mutable, but `result.content` (which is what actually drives the UI) is not part of the hook's output type. ## Expected Behavior One of: 1. **Propagate mutations**: After the hook returns, sync `result.output` back into `result.content` so that mutations are reflected in the UI. 2. **Expose `content`**: Add `result.content` to the hook's output type so plugins can directly modify what gets displayed. 3. **Document as read-only**: If mutation is intentionally unsupported, update the plugin documentation to clarify that `tool.execute.after` is observation-only and cannot modify output. ## Context Discovered while building [opencode-sandbox](https://github.com/isanchez31/opencode-sandbox-plugin) ([npm](https://www.npmjs.com/package/opencode-sandbox)), a plugin that sandboxes agent bash commands using `@anthropic-ai/sandbox-runtime`. The plugin attempts to annotate blocked commands in the output but the annotations never appear. ## Environment - OpenCode: v1.2.1 - OS: Ubuntu 24.04 (Linux 6.8.0) - Plugin: opencode-sandbox@0.1.1
yindo added the needs:compliance label 2026-02-16 18:12:10 -05:00
yindo closed this issue 2026-02-16 18:12:10 -05:00
Author
Owner

@github-actions[bot] commented on GitHub (Feb 14, 2026):

This issue doesn't fully meet our contributing guidelines.

What needs to be fixed:

  • The issue uses a custom format but doesn't use one of the three required templates (Bug Report, Feature Request, or Question). Please submit this using the Bug Report template to ensure consistency with our contribution process.

Please edit this issue to use the correct template within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@github-actions[bot] commented on GitHub (Feb 14, 2026): This issue doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md). **What needs to be fixed:** - The issue uses a custom format but doesn't use one of the three required templates (Bug Report, Feature Request, or Question). Please submit this using the **Bug Report** template to ensure consistency with our contribution process. Please edit this issue to use the correct template within **2 hours**, or it will be automatically closed. If you believe this was flagged incorrectly, please let a maintainer know.
Author
Owner

@isanchez31 commented on GitHub (Feb 14, 2026):

Reopening with the correct bug report template.

@isanchez31 commented on GitHub (Feb 14, 2026): Reopening with the correct bug report template.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#9316