anomalyco / anomalyco/opencode
TUI default in-process transport makes PluginInput.serverUrl report a fabricated localhost:4096 (plugins can't attach)
@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:
-
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 bindWith no network flags,
externalis false, soServer.listen()is never called and there is no socket. -
PluginInput.serverUrlfabricates 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.urlisundefined(no listener), both the clientbaseUrland the publicserverUrlgetter fall back tohttp://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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.