anomalyco / anomalyco/opencode

TUI default in-process transport makes PluginInput.serverUrl report a fabricated localhost:4096 (plugins can't attach)

Open
#39,561 2 comments 0 reactions 1 assignee View on GitHub

@kitlangton is already working on this.

Since Jul 29, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Summary

When the TUI runs with the default in-process transport (no --port / --hostname / --mdns), OpenCode does not bind any TCP listener, but PluginInput.serverUrl still reports a fabricated http://localhost:4096. Plugins that consume ctx.serverUrl as a reachable server (for example to drive the TUI via opencode attach <serverUrl> --session <id>) are handed a dead URL and fail silently.

Root cause

Two connected spots:

  1. Default TUI never binds. packages/opencode/src/cli/cmd/tui.ts (≈ L233–249):

    const external = hasArg("--port") || hasArg("--hostname") || network.mdns === true
    const transport = external
      ? { url: (await client.call("server", network)).url, ... }   // real TCP listener
      : { url: "http://opencode.internal", fetch: createWorkerFetch(client), ... }  // in-process RPC, no bind
    

    With no network flags, external is false, so Server.listen() is never called and there is no socket.

  2. PluginInput.serverUrl fabricates a live-looking URL anyway. packages/opencode/src/plugin/index.ts (≈ L141–161):

    const serverUrl = Server.url                       // undefined when not bound
    baseUrl: serverUrl?.toString() ?? "http://localhost:4096",
    ...
    get serverUrl(): URL {
      return Server.url ?? new URL("http://localhost:4096")   // nothing listening here
    }
    

    When Server.url is undefined (no listener), both the client baseUrl and the public serverUrl getter fall back to http://localhost:4096, which implies a reachable TCP server. There isn't one.

Reproduction

# 1. Start the TUI with no network flags (default).
# 2. Confirm it binds nothing:
lsof -nP -i TCP -sTCP:LISTEN -p <tui-pid>        # → empty

# Compare:
opencode --port 4096   # binds 127.0.0.1:4096 (reachable)

Real-world impact: the @mspiegel31/opencode-cmux subagent-viewer does opencode attach ${serverUrl} --session ${id}. With the fabricated http://localhost:4096 (and the plugin author's documented port 0 workaround), there is no port to attach to, so the viewer aborts silently.

Expected behavior

PluginInput.serverUrl should reflect reality. When no HTTP listener is bound, it should surface the in-process transport placeholder (http://opencode.internal, already used in tui.ts and run.ts) rather than a fabricated localhost:4096, so plugins can detect there is no external TCP server.

Proposed fix

In plugin/index.ts, change both ?? "http://localhost:4096" / ?? new URL("http://localhost:4096") fallbacks to http://opencode.internal. This is behavior-neutral for the plugin client (the in-process transport routes via Server.Default().app.fetch regardless of the baseUrl host) and only makes the reported URL honest.

I have a PR ready that implements this.

Environment

  • opencode 1.18.9 (also present on dev)
  • macOS (darwin/arm64)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.