Repository navigation
Description
Summary
The managed background service binds a hardcoded default port (49374 for
latest/dev/beta/next channels). Service registration
(~/.local/state/opencode/service.json) is per-user, but the port is
machine-global on any shared loopback. Any second client on the same machine —
another Unix user, the Windows host under WSL2 networkingMode=mirrored, or a
--network host container — cannot bind the port. Instead of failing fast with
the real cause, the client spawns serve --service contenders for ~2 minutes
and then reports only Timed out waiting for the background service to start.
The actionable message exists only in the log file, and per the residual gap in
#41696 it can be swallowed entirely by contender overlap.
Environment
Both installations are v2.0.14, channel latest.
Environment A (holds the port):
- opencode version: 2.0.14
- OS: Linux 6.18.33.2-microsoft-standard-WSL2,
networkingMode=Mirroredin.wslconfig - Terminal: TERM=xterm-256color
- Shell: /bin/bash
- Install/channel: latest
- Active plugins: none found in config
Environment B (fails to start):
- opencode version: 2.0.14
- OS: Windows host of the WSL2 above (mirrored mode shares one loopback)
- Terminal: cmd.exe (TERM unavailable: Windows console)
- Shell: cmd
- Install/channel: latest
- Active plugins: none found in config
Reproduction
- In WSL2 with
networkingMode=Mirrored, startopencodeonce and exit the
TUI. The background service (opencode serve --service) persists, listening
on127.0.0.1:49374. - On the Windows host (same loopback), run
opencode. - Wait ~2 minutes.
Same lockout without WSL: on a shared Linux box, run opencode as user A, exit,
then run opencode as user B.
Expected Behavior
Either:
- the default service port is not a machine-global constant — pick an
ephemeral/incremental port and record it in the registration file (it already
storesurl), or - at minimum, fail fast with the real cause and identify the occupying process.
Actual Behavior
Client spawns contenders in a loop, one failure every ~10s for ~2 minutes:
timestamp=2026-09-22T14:47:48.025Z level=ERROR message="cli process failed"
cause="Cause([Fail(Error: Managed service port 49374 on 127.0.0.1 is already in
use by another process. Configure another port with `opencode service set port
<port>` and start the service again. (cause: ServeError (cause: Error: Failed to
start server. Is port 49374 in use?)))])" args="[\"serve\",\"--service\"]" role=cli
(20+ identical repeats), then the only thing the TUI shows:
Error: Timed out waiting for the background service to start
at service.ensure ...
at cli.server-connection.resolve ...
Occupying process, from the WSL side:
$ ss -tlnp | grep 49374
LISTEN 0 512 127.0.0.1:49374 users:(("opencode",pid=12566,fd=10))
$ ps -p 12566
PID STARTED ELAPSED CMD
12566 Tue Sep 22 21:46:33 01:14:39 /home/<user>/.opencode/bin/opencode serve --service
Additional Context
The port is a hardcoded constant (extracted from the v2.0.14 binary):
if (e==="latest"||e==="dev"||e==="beta"||e==="next") return 49374;
if (e==="local") return 49375;
return 1e4 + ...All four release channels share 49374, for every user on the machine.
Collision matrix (fixed default port × shared loopback):
| Scenario | Collides |
|---|---|
| WSL2 mirrored ↔ Windows host | yes (this report) |
| Two Unix users, same machine | yes |
--network host container ↔ host |
yes |
-p 49374:... published container ↔ host |
yes |
| WSL2 NAT default / Docker bridge / full VM | no |
Multi-user impact is a silent lockout, not a data leak. Verified: API
returns 401 unauthenticated; service.json (registration + password) is 0600.
But user B's registration lives in B's home, so B cannot discover A's service;
B gets a bare timeout with no hint that another account is the cause, and the
remediation (opencode service set port) appears only in the log.
Warning on fix direction: #7629 proposes "probe the port and reuse the
existing service." That must not be applied to the default managed port across
users/environments. The service owns sessions, provider credentials,
permissions, and tool execution, so reuse keyed on port occupancy would
(a) require sharing the 0600 password across users or un-authenticating the
API, and (b) bind client A's in-flight work to client B's process lifecycle —
B logging out, wsl --shutdown, or a service auto-update restart (cf. #38567)
would kill A's running tasks with zero control on A's side. Reuse is only safe
when keyed on the client's own registration file (stale-service recovery),
never on port occupancy.
Related: #41696 + PR #41793 addressed surfacing stderr, but the follow-up
comment on #41696 documents a remaining contender-overlap gap that discards the
captured failure until the overall timeout — exactly what I observe on 2.0.14.
#19272 / #21170 / #46263 / #10357 cover adjacent symptom/solution ground; this
issue is the root cause: fixed machine-global default port + per-user
registration.
Workarounds: none applied on side B (user only retried). Side A unaffected.
Activity
github-actions commentedon Sep 22, 2026
This issue might be a duplicate of existing issues. Please check:
networkingMode=mirroredport conflict for the Desktop WSL sidecar — different component but the same shared-loopback collision.Your issue is more specific than any of these — it focuses on the machine-global fixed port as root cause for multi-user lockout and includes a useful warning about the "reuse by port occupancy" anti-pattern — so it may still warrant its own tracking. But linking these in your description (as you've already done for some) will help maintainers triage.
yveming commentedon Sep 22, 2026
Thanks for the triage. Adding context on the linked issues to help with review:
networkingMode=mirrored(ephemeral port handoff failure instead of a fixed port). Together with this issue, that's a second component affected by the same root cause, which supports treating the fixed default port as the structural problem rather than an environment quirk.No further action needed from the bot; leaving this open for the root-cause discussion (fixed port + per-user registration, and the warning against fixing it via "reuse by port occupancy").
p7gg commentedon Sep 23, 2026
NAT mode also collides (not just
networkingMode=mirrored) — and the Desktop app dies with a fatal renderer error instead of a TUI timeoutStill present on 2.0.15. Environment:
archlinux, WSL 2.7.14.0 / kernel 6.18.33.2-2, default NAT networking (.wslconfighas nonetworkingMode, no per-distro override)This contradicts the collision matrix row
WSL2 NAT default … | no: NAT still collides because WSL2 localhost forwarding binds the Windows-side loopback port viawslrelay.exe.Reproduction
opencodeonce. The shared service persists on127.0.0.1:49374.serve --servicecontenders on the same default port; each fails immediately. After ~120 s the renderer throws a fatal error and the UI is unusable.Evidence
WSL side:
Windows side — the NAT forwarder owns the Windows loopback port:
Windows service log — 99 identical failures between 12:34Z and 12:55Z (log capture end), one every ~5–10 s:
Desktop
renderer.log— fatal, unlike the CLI's timeout message:Extra wrinkle: after the host slept overnight, the Windows-side registration pointed at a dead PID (2.0.14), so the app classified the service as
missingand tried to start a fresh one — straight into the collision.Workaround (from the error message, verified): on Windows,
opencode-cli.exe service set port 49375+service start, then relaunch the app. WSL side untouched on 49374.ibaciu6 commentedon Sep 26, 2026
Confirming this on 2.0.18, and the affected client is the desktop app, not the TUI. Also: the collision matrix understates the blast radius — WSL2
networkingMode=natwithlocalhostForwarding=true(the default) collides too.Still present on 2.0.18, via the desktop app
Same root cause, three additions worth folding in.
1. The OpenCode Desktop (Electron) client fails the same way, and the real error never surfaces to the UI. The desktop spawns its own bundled
opencode-cli.exe serve --serviceand retries in the same loop, but the only thing the user ever sees is a fatal renderer error:The actionable
Managed service port 49374 ... is already in usetext stays in the log. A desktop user has noopencode service set portin their workflow, so this reads as "the app is just broken" rather than a port conflict.2. The collision matrix is wrong for NAT mode. This row says NAT does not collide:
| WSL2 NAT default / Docker bridge / full VM | no |
With
networkingMode=natandlocalhostForwarding=true, WSL2 relays a WSL127.0.0.1listener to Windows127.0.0.1viawslrelay.exe, so the two do share that loopback. My.wslconfig:The WSL service and the Windows desktop each believed they owned the port. From Windows:
wslrelay.exeis the occupier, and its existence is the tell that this is WSL2 rather than a genuine second opencode client. From the WSL side the owner looks like an ordinary opencode process:So this is not a narrow mirrored-mode edge case — it is the stock WSL2 configuration, which is the common setup.
3. Loop shape on 2.0.18. 25
cli startingentries and 31 port-conflict failures across two desktop launches, ~10s apart, then the 120s timeout:Verified workaround
Move the Windows side off 49374, leaving WSL untouched. Moving WSL instead is the wrong direction here: the WSL service stays resident, and it is what feeds the loopback relay.
Writes
C:\Users\<user>\.config\opencode\service.json→{ "port": 49375 }. Note the desktop's ownservice getwas{}before this — the port was never configured on either side, both were silently taking the default.Confirmed after the change:
Service ready in ~25ms instead of timing out at 120s, no
renderer.logproduced at all, and both ports coexist (49374 WSL relay + 49375 Windows). Manually starting the CLI also works and serves the UI:The hardcoded-default extraction in this issue still holds on 2.0.18 —
49374appears as a string in the shippedopencode-cli.exe, and the desktop'sapp.asardoes not hardcode it, so the client does follow the configured port. That makes the one-line config change a sufficient fix.Secondary: the desktop debug export ships a stale server log
Worth a separate issue if it is not already tracked, because it actively misleads triage. "Export debug logs" produced a bundle whose
server-1/opencode.logwas a weeks-old log from a previous working run (dated ~7 weeks before the failure), so the bundle contained no trace of the actual failure. The current run's CLI log was in a different location and had to be found separately:The per-run desktop logs (
main.log,renderer.log) were correct; only theserver-1/copy was stale. Anyone triaging from the zip alone would see a clean startup and conclude the cause was elsewhere.Environment
latestnetworkingMode=nat,localhostForwarding=truecmd.exe(Windows side); TERM=xterm-256color (WSL side)latestUsername, machine name, VM GUID, service password, and window identifiers redacted.
alint77 commentedon Oct 4, 2026
Still present on 2.0.22 (Linux aarch64). Hit it on a shared multi-user HPC login node: another Unix user's
opencode serve --serviceheld 127.0.0.1:49374, so mine failed with the generic "Timed out waiting for the background service to start" after ~2 min. The port-in-use cause was only in~/.local/share/opencode/log/opencode.log. Workaround:opencode service set port <free port>.Shra1V32 commentedon Oct 13, 2026
Adding context on multi-tenant / multi-user Linux hosts and how this connects to the open fixes:
Per-user default port via
process.getuid?.()(ServiceConfig.defaultPortinpackages/cli/src/services/service-config.ts):WSL_DISTRO_NAMEintodefaultPort()(hashedPort(${filename(channel)}:${distro})) so WSL distros sharing loopback don't collide on0xC0DE(49374), while keeping the port bind as the per-database single-instance lock (fix(cli): elect managed service by port bind #37572).service.jsonandopencode.dbare per-user (XDG_STATE_HOME/XDG_DATA_HOME), while127.0.0.1is shared across all UIDs in the network namespace.process.getuid?.()into thedefaultPortcalculation alongsidedistrois ideal because the numeric UID is kernel-enforced, stays consistent across all shells/sessions for that user (unlike env vars), and guarantees every contender for the same user still competes for the exact same deterministic port while different OS users on the machine get distinct ports automatically.Secondary multi-user collision & security surface on
/tmp/opencode(packages/util/src/global-roots.ts&packages/util/src/global.ts):opencode service set port <port>), a second OS user on the same Linux host hitsEACCESon/tmp/opencode(Global.Path.tmp) because the first user to start OpenCode creates/tmp/opencodeowned by their UID (drwxrwxr-x) — tracked in agent env: /tmp/opencode advertised as writable scratch but owned by another user #44696, with a per-useropencode-<uid>fix open in fix(util): use a per-user tmp directory #52973.0600service.json+ 256-bit Basic Auth) and SQLite DB (0600viarestrictToOwner) are properly isolated per user,/tmp/opencodeis created withoutmode: 0o700orlstatUID/symlink verification, andpackages/tui/src/editor.ts:17(${Date.now()}.md) /packages/core/src/plugin/vcs/git.ts:562(opencode-untracked-*) write directly toos.tmpdir()with default umask (0644). Detailed in agent env: /tmp/opencode advertised as writable scratch but owned by another user #44696 (comment).mathomp4 commentedon Oct 15, 2026
I'm commenting here because I think this might be related to my issue I'm having.
I was looking to upgrade to opencode v2.0.29 and hit this on a shared multi-user HPC (Linux, not WSL) login node. Another user's
opencode serve --servicealready held127.0.0.1:49374, so mine failed with:On a cluster, many users share each login node, so this will be common. Every user gets the same default port, but the registration in
~/.local/state/opencode/service.jsonis per user. Nobody else can start a service on that node until the first user's service exits.Workaround: each user has to run a one-time
opencode service set port <port>. A per-user port works, for example$((20000 + $(id -u) % 40000)). Doing this for every user on a site-wide install is awkward. I couldn't find an environment variable that overrides the port, and there is no site-level default I can set in a shared install or modulefile.Suggestions (in the order I'd prefer them):
ServiceConfig.defaultPort, as @Shra1V32 proposed above. That would fix multi-user hosts without any configuration.OPENCODE_SERVICE_PORT, so administrators can set it from a modulefile or a system profile. (Which I think I can make programmatic since it's lua modules...)mathomp4 commentedon Oct 15, 2026
@yveming I wonder: should we make the "title" of this issue more broad now? Since both @Shra1V32 and myself hit this on "plain" Linux (compared to WSL)
yveming commentedon Oct 15, 2026
The title contains 'multi-user' and in the section 'Additional Context' listed a scenario 'Two Unix users, same machine'.
mathomp4 commentedon Oct 15, 2026
@yveming Sounds good. Fingers crossed this gets fixed soon! 🙂
mathomp4 commentedon Oct 19, 2026
Replying to myself in case I forget, but this is doable in LMod with an
executestatement to have each user set the port onload. Seems to work on a test machine.Of course, would be good to not need this "hack".
opencodejr commentedon Oct 28, 2026
Thanks for the detailed write-up and the collision matrix! Here's where each part stands:
dev, not yet in a release. On Windows the default service port now moves out of the range WSL mirrored networking reserves (49374 becomes 28894), so the Windows host and the WSL side no longer use the same default port.dev. The port-in-use message now says which process holds the port.What's left is your main request: making the default managed port per-user, or ephemeral and recorded in
service.json, so that two Unix users on one machine, or a--network hostcontainer and its host, don't compete for the same port on Linux. The fixed per-channel port is currently a deliberate default, withopencode service set port <port>as the override, so I've filed this as a feature request. Maintainers will consider it, along with your warning that a service should only be reused through the client's own registration, never just because its port is taken.Related issues:
gparadis commentedon Nov 5, 2026
Still reproducible on v2.2.5 (
latest, freshcurl -fsSL https://opencode.ai/v2/install | bash) on a shared Linux JupyterHub host.Two Unix users on the same host:
127.0.0.1:49374(opencode serve --service, up for days).~/.config/opencode/service.json, runsopencode, and gets the port-in-use error after ~15s. No service starts.User B's log:
Verified workaround:
The client then connects and
opencode modelsworks.opencode --standaloneis a zero-config alternative that uses a private server instead of the managed service.One extra data point for moving the Linux default:
49374is inside the default Linux ephemeral range (net.ipv4.ip_local_port_range = 32768 60999), so an unrelated outbound socket can occupy it at startup too. The Windows/WSL path already remaps default ports >=32768; Linux still uses 49374 unchanged.+1 to folding the UID into
defaultPort(or using an ephemeral port persisted inservice.json).fix(cli): scope managed service port remap to WSL with distinct default