Skip to content

Desktop on Windows sends its bootstrap token to a WSL background service on the same port (invalid_credential on first launch) #16826

Open

Description

@FranciscoMessina

What happened

On Windows, the desktop app (nightly) often shows this error the first time it is opened. Closing it and opening it again usually works.

PrimaryEnvironmentRequestError
    at async Object.beforeLoad (t3code://app/assets/main-CRgQa3HH.js:20:72083)
Caused by:
Error: Error invoking remote method 'desktop:get-local-environment-bearer-token': DesktopLocalEnvironmentAuthSessionBootstrapError: Failed to create the local desktop bearer session.

Diagnosis

The desktop sends its bootstrap token to a different T3 Code server: a background service running inside WSL2, which Windows reaches through WSL localhost forwarding on the same port.

  1. The machine has a T3 Code background service inside WSL2 (Ubuntu): t3code.service, a systemd user unit running t3 serve with T3CODE_HOME=~/.t3 on the Linux side. It listens on the default port, 127.0.0.1:3773.
  2. On a cold start, WSL is still booting when the desktop runs resolveDesktopBackendPort (apps/desktop/src/app/DesktopApp.ts:75). At that moment 127.0.0.1, 0.0.0.0 and :: are all free on 3773, so the desktop picks 3773 and its Windows backend binds 0.0.0.0:3773.
  3. About 2 s later, WSL's localhost forwarding starts and wslrelay.exe binds 127.0.0.1:3773 on Windows. Windows allows both binds, and a connection to 127.0.0.1:3773 goes to the more specific listener, which is the relay to the WSL service.
  4. The desktop's POST http://127.0.0.1:3773/oauth/token reaches the WSL service. That service has a different desktop bootstrap secret, so it returns invalid_credential. isTransientBearerBootstrapError (apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts:27) treats that as final, so the request is not retried and the renderer crashes in beforeLoad.
  5. On the next launch, WSL is already running and the relay already holds 127.0.0.1:3773. The scan skips to 3774 and everything works. That is why it seems to fail only "on first open".

Every launch logged in desktop.trace.ndjson (Oct 4–7, about 20 launches) fits this pattern:

  • Every launch where the desktop picked port 3773 failed.
  • Every launch on 3774 or 3775 succeeded.

While the bug was happening, Windows showed two listeners on 3773:

LocalAddress LocalPort OwningProcess
127.0.0.1    3773      35824   # wslrelay.exe --mode 2 (started 09:54:11 local)
0.0.0.0      3773      37356   # desktop backend (started 09:54:09 local)

and inside WSL:

LISTEN 127.0.0.1:3773  users:(("node-MainThread",pid=594))   # t3 serve (t3code.service, 0.0.44)

Possible fixes, any one of which would help:

  • Have the desktop backend bind 127.0.0.1 when network access is off, so a later 127.0.0.1 bind by another process fails instead of shadowing it.
  • Check after readiness that the server answering on the chosen port is the one the desktop launched. For example, compare the environment id or PID reported by /.well-known/t3/environment, then re-select the port or restart the backend if it isn't.
  • On Windows with WSL, wait for WSL localhost forwarding to settle before scanning, or skip ports a WSL distro is listening on.
  • At minimum, report "another T3 Code server is answering on this port" instead of a generic bootstrap failure.

Steps to reproduce

  1. On Windows 11 with WSL2 (default NAT networking with localhost forwarding), install the T3 Code background service inside a WSL distro with the default port: t3 service install inside WSL, listening on 127.0.0.1:3773.
  2. Run wsl --shutdown, so the distro and its systemd service start cold the next time WSL is used.
  3. Launch the T3 Code desktop app on Windows. If the WSL backend is enabled, the desktop starts WSL during launch, and the service starts with it.
  4. The desktop selects port 3773 before wslrelay binds it. Its /oauth/token exchange gets invalid_credential from the WSL service, and the error screen appears.
  5. Quit and relaunch. The desktop now selects 3774 and works.

Version

Desktop T3 Code (Nightly) 0.0.46-nightly.20261006.2735 (fd1c338). The WSL background service was t3 0.0.44, and the Windows CLI is 0.0.45.

Environment

Windows 11 Pro 10.0.26300 x64, WSL2 Ubuntu (systemd=true), Node v26.8.2

Evidence

# desktop.trace.ndjson, 2026-10-07 (UTC)
12:54:09.842 resolveDesktopBackendPort -> 3773 ("selected backend port via sequential scan")
12:54:09.852 desktop.backendInstance.start {"id":"primary"}
12:54:10.006..12:54:17.742 GET http://127.0.0.1:3773/.well-known/t3/environment  (transport errors, then Success)
12:54:18.330 POST http://127.0.0.1:3773/oauth/token
12:54:18.327 clientRuntime.authorization.bootstrapRemoteBearerSession  Failure  EnvironmentAuthInvalidError: The environment rejected this client's credentials (invalid_credential).
12:54:18.326 desktop.localEnvironmentAuth.getBearerToken               Failure  DesktopLocalEnvironmentAuthSessionBootstrapError
  (repeated 3x within 600 ms)

# Launch port vs. outcome, desktop.trace.ndjson{,.1..10}
2026-10-04T18:00:07 port 3773 -> BEARER FAIL     2026-10-04T18:00:28 port 3774 -> ok
2026-10-05T12:57:35 port 3773 -> BEARER FAIL     2026-10-05T12:58:02 port 3774 -> ok
2026-10-06T17:35:31 port 3773 -> BEARER FAIL     2026-10-06T17:35:48 port 3775 -> ok
2026-10-06T22:01:20 port 3773 -> BEARER FAIL     2026-10-06T22:01:33 port 3774 -> ok
2026-10-07T12:54:09 port 3773 -> BEARER FAIL
(no launch on 3773 succeeded; no launch on 3774/3775 failed)

Related issues

#12918 has the same error screen, but its cause is transient 5xx and reset errors, and its fix retries only those. A rejected credential stays final, so that fix does not cover this case. #6097 covers the desktop and a background service on the same OS sharing a port and database. Here the service runs in WSL with its own T3 home, and the problem is the WSL relay taking over the desktop's port after the port was chosen.

Fix applied or workaround

I moved the WSL service off the default port with a systemd drop-in, which survives t3 update rewriting the unit, and updated it to 0.0.45:

mkdir -p ~/.config/systemd/user/t3code.service.d
printf '[Service]\nEnvironment=T3CODE_PORT=4773\n' > ~/.config/systemd/user/t3code.service.d/port.conf
systemctl --user daemon-reload
t3 update --yes

Afterwards only the desktop backend listens on 3773 on Windows. Setting T3CODE_PORT for the desktop app on the Windows side also works.

Filed by

Claude Code (claude-opus-5-5) via t3 triage

Activity

  1. juliusmarminge commented on Oct 7, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Thanks for the detailed diagnosis. I checked it against the code, and it holds up.

    Root cause

    • resolveDesktopBackendPort (apps/desktop/src/app/DesktopApp.ts:75) picks the first port that is free on 127.0.0.1, 0.0.0.0 and :: (DesktopApp.ts:40). It only checks once, at startup, so it can't see a WSL relay that binds the port about 2 s later.
    • The local URLs always use 127.0.0.1 (apps/desktop/src/backend/DesktopServerExposure.ts:110-111). The bind host is 127.0.0.1 only in local-only mode; any other exposure mode binds 0.0.0.0 (DesktopServerExposure.ts:113-131). Your 0.0.0.0:3773 listener means network access was on for this launch. In that mode, a later, more specific 127.0.0.1:3773 bind by wslrelay.exe takes the desktop's own loopback traffic.
    • Readiness only polls /.well-known/t3/environment until it gets an HTTP response (apps/desktop/src/backend/DesktopBackendManager.ts:70,381-397). It doesn't check that the server answering is the backend the desktop started.
    • isTransientBearerBootstrapError (apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts:27-37) retries only fetch errors, timeouts and 502/503/504. The WSL server's invalid_credential is final, so it surfaces as DesktopLocalEnvironmentAuthSessionBootstrapError.

    Proposed fix

    1. Keep binding loopback when network access is off. That already happens in local-only mode. When it's on, consider binding 127.0.0.1 as well, or otherwise making sure another process can't shadow loopback.
    2. After readiness, check that the backend is the one we launched, for example by comparing the environment id or PID from /.well-known/t3/environment. On a mismatch, restart on the next free port.
    3. Show a clearer error when the bearer bootstrap is rejected ("another T3 Code server is answering on port N") instead of the generic crash.

    Related: #9056, #12919, #7702, #10360.

    Workaround
    Stop the WSL t3code.service, or move it to a different port. Then relaunch the desktop app. A second launch also works, because the scan skips the port the relay holds.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 7, 2026
  3. rhaoio commented on Oct 21, 2026

    @rhaoio

    Hey guys! just had an issue with t3code on Windows and WSL, below is from t3 triage with Claude. Thanks! (enjoying the software)

    Another instance of this race, with a different user-visible symptom: instead of the bootstrap token landing on the wrong server, a saved connection to the WSL
    background service stops working
    because localhost:3773 now resolves to the Windows backend.

    Setup

    • Windows 10 (10.0.19045), WSL 2.7.12, distro Ubuntu-22.04, Node v26.8.2
    • Desktop app T3 Code (Beta) 0.1.0 (stable), Windows primary backend on 127.0.0.1:3773
    • t3code.service systemd user unit inside the distro, t3 serve 0.1.0 on 127.0.0.1:3773 (WSL side), reached from Windows through WSL localhost forwarding,
      saved in the desktop app as http://localhost:3773/
    • Desktop settings: wslBackendEnabled: true, wslDistro: "Ubuntu-22.04", wslOnly absent

    What happened

    On a cold start (WSL VM not running), the desktop app's WSL probe boots the VM, systemd starts the service, and the service needs ~8 s to listen. The desktop's
    resolveDesktopBackendPort scan runs ~0.2 s after the probe, finds 3773 free, and the Windows backend binds 127.0.0.1:3773. When the WSL service starts
    listening, forwarding to Windows 127.0.0.1:3773 cannot be established (the Windows backend already holds it; [::1]:3773 is not reachable either).

    The saved connection then connects to the Windows backend and the client blocks it:

    http://localhost:3773/ · Connection failed: Connected environment a1e7accc-… does not match 7e28d6e2-…
    

    Windows backend trace (server.trace.ndjson), 09:14 to 09:24 local: 203 + 28 ConnectionBlockedError with that message, and the WSL service trace shows zero
    inbound Electron requests after the restart. The previous evening, with WSL already running before the app launched, the service held 3773, the Windows backend
    scanned to 3774, and the WSL service trace shows ~90 Electron requests per half hour on localhost:3773.

    Timeline today:

    Time (local) Event
    09:14:10 WSL VM boots (triggered by the desktop app's desktop.wsl.isAvailable probe at 09:14:12.5), systemd starts t3code.service
    09:14:12.8 selected backend port via sequential scan port 3773 (desktop trace)
    09:14:18 WSL service: Listening on http://127.0.0.1:3773
    09:14:16 onward ConnectionBlockedError: Connected environment <windows> does not match <wsl>

    A secondary effect of the same setup: enabling Settings > Connections > WSL backend starts a second t3 serve against the same WSL ~/.t3, which exits 78:

    ServerAlreadyRunningError: A T3 Code server already owns ~/.t3/userdata. Stop that server, or use a separate T3 home and pair with it, then retry. No server was
    stopped.
    

    (server-child-wsl_Ubuntu-22.04.log, 09:16:17 today and 09:01:00 on Oct 19). Expected given #16102, but it means a user who runs the service has no working
    in-app WSL option, and the Connections UI hint ("Stop it, or pair with it instead") points back at the connection that this race breaks.

    Workaround that worked

    Pin the service to a port the desktop never scans first, via a systemd drop-in, then re-pair:

    mkdir -p ~/.config/systemd/user/t3code.service.d
    printf '[Service]\nEnvironment=T3CODE_PORT=3790\n' > ~/.config/systemd/user/t3code.service.d/port.conf
    systemctl --user daemon-reload && systemctl --user restart t3code.service
    

    After this, http://localhost:3790/.well-known/t3/environment from Windows returns the WSL environment id regardless of startup order, and the managed T3
    Connect tunnel re-registered on the new port.

    Suggestion

    Besides the fix discussed above for the token path, the desktop could avoid the collision at the source: when a WSL distro is enabled or probed, wait for the VM
    boot (or for forwarding on 3773 to settle) before running the port scan, or default the Windows backend to a port other than the server CLI's DEFAULT_PORT so a WSL/npm t3 serve on 3773 and the desktop never contend.


    Diagnosed with t3 triage (Claude Fable 5.1 via Claude Code).

  4. juliusmarminge commented on Oct 22, 2026

    @juliusmarminge
    Member

    Fresh triage against main b5a226e49c: the literal cold-start sequence stops at wsl.exe --shutdown on this Linux VM (wsl.exe: command not found), so the Windows relay race was not reproduced here. Current source already binds loopback with network access off; that source check is not runtime proof.

    The existing fix #21102 targets this failure and still lists native Windows/WSL verification as missing. No competing fix was made. Please provide a cold-start run on a current Windows nightly, the Network access setting, and sanitized desktop/backend startup logs plus the Windows listener table at failure. Please distinguish the original invalid_credential failure from the saved-connection environment-ID mismatch in the newer comment; omit tokens and pairing URLs.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.needs more infoInitial triage showed no bug. Awaiting more infovia-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions