Build: 0.0.34-nightly.20260812.1072 (T3 Code Nightly)
Platform: macOS (darwin 25.6.0), Apple silicon
Summary
Each run of T3 Code Desktop mints a new local bearer session and never releases it. Settings → Connections → Authorized clients gains one extra greyed-out T3 Code Desktop entry per restart, indistinguishable from a genuine second device. They are only removed by revoking them manually or by waiting out the 30-day TTL.
Steps to reproduce
Precondition: Network access must be on, otherwise the section is not rendered. It is gated on isLocalBackendRemotelyReachable (src/components/settings/ConnectionsSettings.tsx:3060), defined at :2330 as isLocalBackendNetworkAccessible || tailscaleHttpsEndpoint?.status === "available".
- Open T3 Code Desktop. Authorized clients shows one
T3 Code Desktop marked This device.
- Quit the app fully, reopen it.
- Authorized clients shows two
T3 Code Desktop entries: the current one, plus an unconnected leftover from the previous run.
The gate is presentational only — the row is written server-side when the token exchange succeeds, so entries accumulate whether or not the section is visible.
Evidence
Active rows in ~/.t3/userdata/state.sqlite → auth_sessions, matching the UI one-for-one:
| session_id |
client_label |
issued_at |
last_connected_at |
49f001e6 |
iPhone |
2026-08-10T01:57:18Z |
2026-08-12T05:37:42Z |
3632dc34 |
app.t3.codes |
2026-08-10T05:06:15Z |
2026-08-10T05:06:22Z |
f7867207 |
T3 Code Desktop |
2026-08-12T06:10:29.259Z |
2026-08-12T06:10:29.629Z |
f7edcaf8 |
T3 Code Desktop |
2026-08-12T06:56:49.851Z |
2026-08-12T06:58:33.547Z |
~/.t3/userdata/server-runtime.json reports startedAt: 2026-08-12T06:56:49.440Z. The live session f7edcaf8 was issued 411 ms later. The stale f7867207 was issued at 06:10:29Z during the previous run; its last_connected_at advanced only once, 370 ms after issue, and never again.
Over ~12 days this install accumulated 43 rows labelled T3 Code Desktop. 41 are revoked, each carrying a distinct revoked_at — a single bulk revoke stamps every affected row with one shared timestamp (src/persistence/AuthSessions.ts:297, src/auth/SessionStore.ts:867), so these were 41 separate revocation operations.
Root cause
Line numbers are from the sourcemaps shipped inside app.asar.
apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts:48 caches the bearer token in Ref.make(Option.none<string>()) — process memory only, never persisted.
- On each run that
Ref starts empty, so the first getBearerToken call reaches line 70 and calls bootstrapRemoteBearerSession({ label: "T3 Code Desktop", deviceType: "desktop" }).
bootstrapRemoteBearerSession (packages/client-runtime/src/authorization/remote.ts:64) is an unconditional RFC 8693 token exchange against POST /oauth/token. It has no reuse path; a successful exchange always inserts a new auth_sessions row with a 30-day TTL.
- Nothing releases the session when the app quits. By contrast
src/cli/connect.ts:278 and src/cli/project.ts:220 release theirs via environmentAuth.revokeSession(...) in a finalizer; the desktop path has no equivalent.
- Every path that writes
revoked_at is explicitly driven — POST /api/auth/clients/revoke (src/auth/http.ts:405), POST /api/auth/clients/revoke-others (:425), t3 auth session revoke (src/cli/auth.ts:216), and the two CLI finalizers above. There is no shutdown hook, startup sweep or idle reaper covering the desktop session.
listActiveSessionRows (src/persistence/AuthSessions.ts:246) filters only on revoked_at IS NULL AND expires_at > now (:267-268), so an orphan stays listed for its full 30-day TTL.
Issuance is lazy and mutex-guarded (Semaphore.make(1)), so a run leaks at most one session — but any run that reaches the local environment leaks exactly one.
The renderer is not a second source of these rows. readDesktopPrimaryBearerToken (src/environments/primary/desktopAuth.ts:12) memoises bridge.getLocalEnvironmentBearerToken(), reusing the main process's token. The other bootstrapRemoteBearerSession call in the web client (src/connection/platform.ts:324) serves desktop-local secondary backends and tags sessions via clientMetadata() (:116), which always sets os: navigator.platform. All 43 leaked rows have client_os NULL, matching DesktopLocalEnvironmentAuth.ts:73-76, which sets only label and deviceType; every other label in the table carries a non-null client_os.
Impact
Primarily a correctness and UI problem: the client list misrepresents how many clients are authorized, and a stale row is visually identical to a real second device, so there is no safe way for a user to tell which entries matter. Cleanup is manual and recurs after every restart.
Note that auth_sessions stores session metadata only — the bearer token itself is a signed value returned to the client (src/auth/SessionStore.ts:613) and, on the desktop path, held solely in the exited process's memory. An orphaned row therefore does not by itself expose a usable credential; it means the server will continue to honour that token for 30 days should it have been captured or retained anywhere.
Scope not covered
Reproduced on clean quit-and-reopen. Not separately characterised for updater-driven restart, crash, or forced termination, which may take different lifecycle paths.
Build:
0.0.34-nightly.20260812.1072(T3 Code Nightly)Platform: macOS (darwin 25.6.0), Apple silicon
Summary
Each run of T3 Code Desktop mints a new local bearer session and never releases it. Settings → Connections → Authorized clients gains one extra greyed-out T3 Code Desktop entry per restart, indistinguishable from a genuine second device. They are only removed by revoking them manually or by waiting out the 30-day TTL.
Steps to reproduce
Precondition: Network access must be on, otherwise the section is not rendered. It is gated on
isLocalBackendRemotelyReachable(src/components/settings/ConnectionsSettings.tsx:3060), defined at:2330asisLocalBackendNetworkAccessible || tailscaleHttpsEndpoint?.status === "available".T3 Code Desktopmarked This device.T3 Code Desktopentries: the current one, plus an unconnected leftover from the previous run.The gate is presentational only — the row is written server-side when the token exchange succeeds, so entries accumulate whether or not the section is visible.
Evidence
Active rows in
~/.t3/userdata/state.sqlite→auth_sessions, matching the UI one-for-one:49f001e63632dc34f7867207f7edcaf8~/.t3/userdata/server-runtime.jsonreportsstartedAt: 2026-08-12T06:56:49.440Z. The live sessionf7edcaf8was issued 411 ms later. The stalef7867207was issued at06:10:29Zduring the previous run; itslast_connected_atadvanced only once, 370 ms after issue, and never again.Over ~12 days this install accumulated 43 rows labelled
T3 Code Desktop. 41 are revoked, each carrying a distinctrevoked_at— a single bulk revoke stamps every affected row with one shared timestamp (src/persistence/AuthSessions.ts:297,src/auth/SessionStore.ts:867), so these were 41 separate revocation operations.Root cause
Line numbers are from the sourcemaps shipped inside
app.asar.apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts:48caches the bearer token inRef.make(Option.none<string>())— process memory only, never persisted.Refstarts empty, so the firstgetBearerTokencall reaches line 70 and callsbootstrapRemoteBearerSession({ label: "T3 Code Desktop", deviceType: "desktop" }).bootstrapRemoteBearerSession(packages/client-runtime/src/authorization/remote.ts:64) is an unconditional RFC 8693 token exchange againstPOST /oauth/token. It has no reuse path; a successful exchange always inserts a newauth_sessionsrow with a 30-day TTL.src/cli/connect.ts:278andsrc/cli/project.ts:220release theirs viaenvironmentAuth.revokeSession(...)in a finalizer; the desktop path has no equivalent.revoked_atis explicitly driven —POST /api/auth/clients/revoke(src/auth/http.ts:405),POST /api/auth/clients/revoke-others(:425),t3 auth session revoke(src/cli/auth.ts:216), and the two CLI finalizers above. There is no shutdown hook, startup sweep or idle reaper covering the desktop session.listActiveSessionRows(src/persistence/AuthSessions.ts:246) filters only onrevoked_at IS NULL AND expires_at > now(:267-268), so an orphan stays listed for its full 30-day TTL.Issuance is lazy and mutex-guarded (
Semaphore.make(1)), so a run leaks at most one session — but any run that reaches the local environment leaks exactly one.The renderer is not a second source of these rows.
readDesktopPrimaryBearerToken(src/environments/primary/desktopAuth.ts:12) memoisesbridge.getLocalEnvironmentBearerToken(), reusing the main process's token. The otherbootstrapRemoteBearerSessioncall in the web client (src/connection/platform.ts:324) serves desktop-local secondary backends and tags sessions viaclientMetadata()(:116), which always setsos: navigator.platform. All 43 leaked rows haveclient_osNULL, matchingDesktopLocalEnvironmentAuth.ts:73-76, which sets onlylabelanddeviceType; every other label in the table carries a non-nullclient_os.Impact
Primarily a correctness and UI problem: the client list misrepresents how many clients are authorized, and a stale row is visually identical to a real second device, so there is no safe way for a user to tell which entries matter. Cleanup is manual and recurs after every restart.
Note that
auth_sessionsstores session metadata only — the bearer token itself is a signed value returned to the client (src/auth/SessionStore.ts:613) and, on the desktop path, held solely in the exited process's memory. An orphaned row therefore does not by itself expose a usable credential; it means the server will continue to honour that token for 30 days should it have been captured or retained anywhere.Scope not covered
Reproduced on clean quit-and-reopen. Not separately characterised for updater-driven restart, crash, or forced termination, which may take different lifecycle paths.