SSH launcher ignores a custom T3CODE_HOME and starts a second environment instead of reusing the service#17039

openedOct 8, 2026bySsliva0

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:

That host reaches <label>, a different machine. Add it as its own environment instead.

It is the same machine. The SSH launcher did not find the service, started a second server on the host against a new ~/.t3 with 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 reads T3CODE_HOME or the installed service's home:

  • Discovery. DEFAULT_SERVER_HOME="$HOME/.t3" and DEFAULT_RUNTIME_FILE point 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 as external.
  • Launch. serve --base-dir "$DEFAULT_SERVER_HOME" (tunnel.ts#L702). The flag wins over T3CODE_HOME (cli/app.ts#L200), so the server lands in ~/.t3 even though T3CODE_HOME=~/.config/t3 is in its environment (see Evidence). Exporting T3CODE_HOME on the host cannot fix this.
  • Pairing. auth pairing create --base-dir "$HOME/.t3" (tunnel.ts#L724-L733). Pairing also runs as a non-login sh -s (L939) while launch runs sh -l -s (L878), so the two do not necessarily see the same environment.
  • Launcher state and runtime cache are under ~/.t3 too (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 ensureSshEnvironment has 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 for t3 update and t3 service status.

Possible directions:

  1. Resolve the home once on the remote: T3CODE_HOME if 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.
  2. When adding a route to an existing environment, adopt a running server instead of launching one, or stop the managed server that was just launched when the environment ID does not match. Reword the message so it does not say "a different machine" when the host may just have a different T3 home.

Steps to reproduce

  1. On a Linux host, install the background service with a non-default home, for example T3CODE_HOME=~/.config/t3 t3 service install, and connect it through T3 Connect.
  2. Make sure ~/.t3 does not exist on the host.
  3. In the desktop app on another machine, add an SSH route to that environment from Settings → Connections.
  4. The route fails with "a different machine". On the host, ~/.t3 now exists and a second t3 serve --base-dir ~/.t3 is 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

shell
1# Home directory shown as ~
2
3# Installed service unit
4Environment=T3CODE_HOME=~/.config/t3
5ExecStart=~/.config/t3/runtime/versions/0.0.46-nightly.20261007.2787/t3 __service-launcher
6
7# ~/.config/t3/userdata/server-runtime.json, the file the launcher should have found
8{"version":1,"pid":<service pid>,"port":3773,"origin":"http://127.0.0.1:3773","serviceManaged":true}
9
10# Server the SSH launcher started, still running after the client rejected it
11# (ps and /proc/<pid>/environ)
12PPID 1 ~/.t3/runtime/versions/0.0.46-nightly.20261007.2787/t3 serve --host 127.0.0.1 --port 3774 --base-dir ~/.t3
13T3CODE_HOME=~/.config/t3
  • #7921 (closed, not merged) is the only prior work on this. It found the service's T3CODE_HOME through systemctl --user show t3code.service and /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 #15467 without this piece, and adding an SSH route to a T3 Connect environment is the flow that hits this bug. The /proc approach is Linux-only and needs the service to be running; the unit file covers launchd and a stopped service.
  • #15986, with open PRs #13521 and #13341: the launcher mistakes its own managed server for the service because both use ~/.t3/userdata/server-runtime.json. Different bug in the same discovery block; a fix here touches the same lines.
  • #5749: the launcher replaces an external service with a competing runtime.
  • #10810: XDG base directories on Linux. A T3CODE_HOME under $XDG_CONFIG_HOME is that layout set up by hand.

None of these report a non-default home on the SSH host.

Fix applied or workaround

Symlink ~/.t3 to the custom home (ln -s ~/.config/t3 ~/.t3). With that in place, the launcher found the service and recorded it as external on 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.json without 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.

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/.t3 and never reads T3CODE_HOME or the installed service's home:
    • Discovery: DEFAULT_SERVER_HOME="$HOME/.t3" and DEFAULT_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).
  • --base-dir takes precedence over T3CODE_HOME for serve, which falls back to ~/.t3 only when neither is set (cli/config.ts#L319-L327, os-jank.ts#L105-L111). So exporting T3CODE_HOME on 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 ~/.t3 therefore 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-login sh -s (L939), so a T3CODE_HOME set only in a login profile wouldn't reach pairing anyway.
  • The "a different machine" check runs only after ensureSshEnvironment has launched the server and issued a pairing token (web/connection/platform.ts#L190-L214). That error path doesn't call ssh.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.service and the plist at ~/Library/LaunchAgents/com.t3tools.t3code.service.plist both carry T3CODE_HOME (bootService.ts#L108, L241-L247, L309-L314), and bootServiceBaseDirOf already parses it back out (L66). There's no service.env on main. That file only exists in open #12633, under $T3CODE_HOME, so it wouldn't help the launcher find the home.

One small correction: the cli/app.ts#L200 link in the report is the t3 app command. The serve precedence is the cli/config.ts range 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 ~/.t3 with 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)

  1. 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 be T3CODE_HOME if set, then T3CODE_HOME from the installed unit or plist (the format bootServiceBaseDirOf reads), 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.
  2. When the environment ID doesn't match, call ssh.disconnect before returning the error. The stop script already skips external servers (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 ~/.t3 before 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

  • #15986, with open PRs #13521 and #13341: the same discovery block. A fix here will likely conflict with those.
  • #5749: the launcher replaces an external service.
  • #7921 (closed, not merged): it resolved the service home via systemctl --user show plus /proc/<pid>/environ. That piece didn't land with #15467.
  • #10810: XDG paths.
  • #14115: servers sharing one base dir, the opposite failure mode.

I found no open PR that makes the SSH launcher honor a custom home.

Fresh triage: this is an ordinary bug report, not a reported exploitable vulnerability. Checked all 948 open issues across 19 pages; no duplicate of the custom-home SSH route failure found. Source inspected at 1535b12: discovery and launch still use ~/.t3, and REMOTE_PAIRING_SCRIPT runs auth pairing create against that same hardcoded home. Correct reuse of the custom-home service also requires pairing against its credential store. The automated flow forbids changes to authentication or credentials, so this needs maintainer handling. No runtime reproduction performed, no fix written, and the issue remains open.