Repository navigation
SSH launcher ignores a custom T3CODE_HOME and starts a second environment instead of reusing the service #17039
Description
Activity
Note
Grok responding on behalf of Julius.
Thanks for the detailed report. I checked it against
main@83a82a4, and the core of it holds up.Confirmed in code
- Every remote SSH script hardcodes
$HOME/.t3and never readsT3CODE_HOMEor the installed service's home:- Discovery:
DEFAULT_SERVER_HOME="$HOME/.t3"andDEFAULT_RUNTIME_FILE=…/userdata/server-runtime.json(tunnel.ts#L545-L547). - Launch:
serve … --base-dir "$DEFAULT_SERVER_HOME"(L702). - Pairing:
auth pairing create --base-dir "$HOME/.t3"(L723-L734). - Archive runtime cache (L449-L458) and launcher state for the stop/log scripts (L737, L760).
- Discovery:
--base-dirtakes precedence overT3CODE_HOMEforserve, which falls back to~/.t3only when neither is set (cli/config.ts#L319-L327, os-jank.ts#L105-L111). So exportingT3CODE_HOMEon the host can't redirect the launched server.- Both the runtime file and the environment ID live under
<home>/userdata/(config.ts#L164-L165). A server started on a fresh~/.t3therefore can't see the service's runtime file and comes up with its own environment ID. - Launch runs as
sh -l -s(L878) and pairing as a non-loginsh -s(L939), so aT3CODE_HOMEset only in a login profile wouldn't reach pairing anyway. - The "a different machine" check runs only after
ensureSshEnvironmenthas launched the server and issued a pairing token (web/connection/platform.ts#L190-L214). That error path doesn't callssh.disconnect, so nothing tears down the tunnel or the managed server at that point. I didn't trace whether something stops it later. For example, the desktop tunnel manager's finalizer does run the stop script for tunnels still open when it shuts down. - The service's home can be read without asking the user. The systemd unit at
~/.config/systemd/user/t3code.serviceand the plist at~/Library/LaunchAgents/com.t3tools.t3code.service.plistboth carryT3CODE_HOME(bootService.ts#L108, L241-L247, L309-L314), andbootServiceBaseDirOfalready parses it back out (L66). There's noservice.envonmain. That file only exists in open fix(server): merge T3-home service.env into the background launcher #12633, under$T3CODE_HOME, so it wouldn't help the launcher find the home.
One small correction: the
cli/app.ts#L200link in the report is thet3 appcommand. Theserveprecedence is thecli/config.tsrange above. It behaves the same way.Root cause. The SSH launcher assumes the remote home is
~/.t3. When the service uses a different home, discovery misses it, and a second managed server starts on~/.t3with a new environment ID. Adding a route to the existing environment then fails the ID check, and that server is left running.Possible fixes (untested)
- Resolve the remote home once per connection and use it everywhere: discovery,
serve --base-dir, pairing, the runtime cache and probably launcher state. The order would beT3CODE_HOMEif set, thenT3CODE_HOMEfrom the installed unit or plist (the formatbootServiceBaseDirOfreads), then$HOME/.t3. Pairing should get the same resolved value, either passed in or stored in the state dir, so it doesn't depend on which shell type sees the env. - When the environment ID doesn't match, call
ssh.disconnectbefore returning the error. The stop script already skipsexternalservers (L736-L755). Separately, the wording could hint that the host may be using a different T3 home rather than being a different machine.
Workaround. Use
ln -s ~/.config/t3 ~/.t3, as you found. Stop any server the launcher already started under a real~/.t3before you swap the directory. On shutdown, a server clears<home>/userdata/server-runtime.json(server.ts#L748), and through the symlink that file belongs to the service. Open #9045 targets that.Related
- SSH remote: client kills its own managed server on every reconnect (default-runtime adoption check) #15986, with open PRs fix(ssh): reconnecting no longer restarts the remote server #13521 and fix(ssh): stop killing the managed server on reconnect #13341: the same discovery block. A fix here will likely conflict with those.
- SSH reconnect can replace an external T3 service with a competing managed runtime #5749: the launcher replaces an external service.
- feat(connection): support multiple routes per environment #7921 (closed, not merged): it resolved the service home via
systemctl --user showplus/proc/<pid>/environ. That piece didn't land with feat(clients): reach one environment over several routes #15467. - Honor XDG Base Directory paths on Linux #10810: XDG paths.
- Multiple servers share one --base-dir and each resumes the others' live threads #14115: servers sharing one base dir, the opposite failure mode.
I found no open PR that makes the SSH launcher honor a custom home.
- Every remote SSH script hardcodes
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.via-triageFiled through npx t3 triageFiled through npx t3 triage
on Oct 8, 2026
What happened
The host runs the background service with a custom home (
T3CODE_HOME=~/.config/t3) and is already connected through T3 Connect. Adding an SSH route to that environment from Settings → Connections fails with:It is the same machine. The SSH launcher did not find the service, started a second server on the host against a new
~/.t3with its own environment ID, and left that server running after the client rejected it. Reproduces over LAN and Tailscale.Diagnosis
The SSH launch scripts assume the remote server's home is
$HOME/.t3. Nothing in them readsT3CODE_HOMEor the installed service's home:DEFAULT_SERVER_HOME="$HOME/.t3"andDEFAULT_RUNTIME_FILEpoint at~/.t3/userdata/server-runtime.json(tunnel.ts#L545-L547). The service writes its runtime file under~/.config/t3/userdata/, so the launcher never sees it and never adopts the service asexternal.serve --base-dir "$DEFAULT_SERVER_HOME"(tunnel.ts#L702). The flag wins overT3CODE_HOME(cli/app.ts#L200), so the server lands in~/.t3even thoughT3CODE_HOME=~/.config/t3is in its environment (see Evidence). ExportingT3CODE_HOMEon the host cannot fix this.auth pairing create --base-dir "$HOME/.t3"(tunnel.ts#L724-L733). Pairing also runs as a non-loginsh -s(L939) while launch runssh -l -s(L878), so the two do not necessarily see the same environment.~/.t3too (L449-L458, L545, L737, L760). The host downloads a second copy of the runtime version the service already has under~/.config/t3/runtime/versions.The new server generates a new environment ID, and the client rejects it (platform.ts#L212). That check runs after
ensureSshEnvironmenthas launched the server and minted a pairing token, and nothing stops the server on a mismatch, so it keeps running. The message's advice is wrong here: following it adds a second environment on the same host.The service's home is available without asking the user. The installed unit or plist records it, and
bootServiceBaseDirOf(bootService.ts#L66) already reads it back fort3 updateandt3 service status.Possible directions:
T3CODE_HOMEif set, else the installed service's home, else~/.t3. Use it for discovery,serve, pairing, launcher state, and the runtime cache, and run pairing in the same login shell as launch.Steps to reproduce
T3CODE_HOME=~/.config/t3 t3 service install, and connect it through T3 Connect.~/.t3does not exist on the host.~/.t3now exists and a secondt3 serve --base-dir ~/.t3is listening on 3774.Version
0.0.46-nightly.20261007.2787 (service and desktop). Code references are
main@ 83a82a4.Environment
Host: CachyOS Linux x64, systemd user service, Node 26.8.2. Client: desktop app on a second Linux machine, over LAN and Tailscale.
Evidence
Related issues
T3CODE_HOMEthroughsystemctl --user show t3code.serviceand/proc/<pid>/environ, then paired against that home. It was closed for scope as part of a large multi-route PR. Multi-route support later landed in feat(clients): reach one environment over several routes #15467 without this piece, and adding an SSH route to a T3 Connect environment is the flow that hits this bug. The/procapproach is Linux-only and needs the service to be running; the unit file covers launchd and a stopped service.~/.t3/userdata/server-runtime.json. Different bug in the same discovery block; a fix here touches the same lines.T3CODE_HOMEunder$XDG_CONFIG_HOMEis that layout set up by hand.None of these report a non-default home on the SSH host.
Fix applied or workaround
Symlink
~/.t3to the custom home (ln -s ~/.config/t3 ~/.t3). With that in place, the launcher found the service and recorded it asexternalon port 3773.If the launcher already started a server against a real
~/.t3, stop that server before replacing the directory. On graceful shutdown a server removes<home>/userdata/server-runtime.jsonwithout checking that it wrote it (server.ts#L748; #9045 proposes that check). Through the symlink, that file is the service's.Filed by
Claude Code (Claude Opus 5.5) in T3 Code, building on a first pass by Codex (GPT-6) via
t3 triage.