Skip to content

tool.execute.after hook: UI renders metadata.output instead of output, ignoring mutations #13575

Closed

Description

@isanchez31

Description

The tool.execute.after plugin hook can mutate output.output on the result object, and the mutation correctly propagates to Session.updatePart({ state: { output: value.output.output } }). However, the TUI renders metadata.output instead of state.output for bash tools, so the mutation is never visible to the user.

In packages/opencode/src/cli/cmd/tui/routes/session/index.tsx, the Bash component reads:

const output = createMemo(() => stripAnsi(props.metadata.output?.trim() ?? ""))

But the bash tool sets metadata.output as a separate string copy at execution time, before the hook runs:

// packages/opencode/src/tool/bash.ts
return {
  title: params.description,
  metadata: {
    output: output, // ← snapshot before hook
    exit: proc.exitCode,
  },
  output, // ← this gets mutated by the hook, but UI doesn't read it
}

So even though the hook successfully mutates result.output, the UI never shows the change because it reads metadata.output which still has the pre-hook value.

Plugins

[email protected]

OpenCode version

1.2.1

Steps to reproduce

  1. Create a plugin that appends text to 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⚠️ Annotation from plugin"
  }
}
  1. Run a bash command that produces "Operation not permitted" in its output
  2. Observe that the annotation does not appear in the TUI output panel

Expected: The annotation appears in the displayed output.

Actual: The TUI renders metadata.output (pre-hook snapshot) instead of state.output (post-hook mutated value).

Screenshot and/or share link

No response

Operating System

Ubuntu 24.04 (Linux 6.8.0-100-generic)

Terminal

Ghostty

Activity

  1. added
    opentuiThis relates to changes in v1.0, now that opencode uses opentui
    on Feb 14, 2026
  2. isanchez31 commented on Feb 14, 2026

    @isanchez31
    Author

    Local Validation

    I patched the installed plugin to log diagnostic data after the tool.execute.after hook runs. Here's the sanitized output from two consecutive blocked bash commands (curl google.com):

    === SANDBOX DEBUG 2026-02-14T10:09:52.286Z ===
    tool: bash
    mutated: true
    output.output (first 200): bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted
    
    ⚠️ [opencode-sandbox] Command blocked or partially blocked by sandbox restrictions. Adjust config in .opencode/sandbox.json or OPENCODE_S
    output.metadata keys: ["output","exit","description","truncated"]
    output.metadata.output (first 200): bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted
    
    metaBefore === output.metadata.output: true
    output.output === output.metadata.output: false
    === END ===
    

    Key findings

    Field Value
    mutated true — the hook successfully modified output.output
    output.output Contains the appended ⚠️ [opencode-sandbox] ... annotation
    output.metadata.output Only contains the original bwrap: ... Operation not permitted — no annotation
    metaBefore === output.metadata.output true — metadata.output was never changed by the hook
    output.output === output.metadata.output false — they diverge after mutation

    Debug patch used

    "tool.execute.after": async (input, output) => {
      if (input.tool !== "bash") return;
      const text = output.output ?? "";
      const metaBefore = output.metadata?.output;
      if (text.includes("Operation not permitted") || text.includes("Connection blocked by network allowlist")) {
        output.output = text + "\n\n⚠️ ...annotation...";
      }
      // Diagnostic logging
      const fsSync = require("fs");
      fsSync.appendFileSync("/tmp/sandbox-debug.log", [
        "mutated: " + (output.output !== text),
        "output.output === output.metadata.output: " + (output.output === output.metadata?.output),
        "metaBefore === output.metadata.output: " + (metaBefore === output.metadata?.output),
      ].join("\n"));
    }

    Conclusion

    This confirms that:

    1. The hook does mutate output.output correctly
    2. output.metadata.output is a separate string reference assigned before the hook runs
    3. Since JS strings are immutable, reassigning output.output creates a new reference without affecting metadata.output
    4. The TUI reads metadata.output → the annotation is never displayed

    Environment: OpenCode 1.2.1, Linux (bwrap sandbox), [email protected]

  3. github-actions commented on Apr 16, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 90 days of no activity. If the issue is still relevant please open a new one.

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

Metadata

Metadata

Assignees

Labels

opentuiThis relates to changes in v1.0, now that opencode uses opentui

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions