pingdotgg / pingdotgg/t3code

[Bug]: Open in editor dead-ends at "No SSH route" for a same-machine browser reaching the server by LAN IP

Open
#7,530 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
23k
Forks
5.9k
Avg merge
11h 14m
Merged PRs (30d)
357

Description

What happened

I couldn't open VS Code from a worktree — the Open menu said "No SSH route to MBP14". It seemed to happen every time I was in a worktree.

The server and the browser are both on the same Mac. I run npx t3@nightly serve --host 0.0.0.0 (so my iPhone can reach it too), and the browser tab I was using pointed at the machine's LAN address rather than localhost.

Diagnosis

Open-in-editor is disabled with a dead-end "No SSH route" state for a browser running on the same machine as the server, purely because the URL host is the machine's own LAN IP instead of a loopback name. Local exec would have worked; there is no fallback to it.

Two conditions combine:

1. A same-machine browser on a LAN-IP URL is classified as remote.

resolveRemoteOpenState (apps/web/src/remoteOpen.ts:64-78) only keeps local-exec for a primary target when the host is loopback, against a fixed set in apps/web/src/environments/primary/target.ts:76:

const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]);

The server binds 0.0.0.0:3773, and the browser reached it at http://192.168.0.112:3773 — which is this same Mac's en0 address. 192.168.0.112 is not in that set, so the code decides the viewer is on a different machine and switches to SSH deep-link mode.

2. There is no SSH route to fall back on, so it dead-ends.

RemoteOpenTargets.resolveTargets() (apps/server/src/environment/RemoteOpenTargets.ts:37-44) advertises nothing when no sshd is listening:

if (!sshdListening) {
  return [];
}

macOS Remote Login is off and Tailscale is not installed, so there is no MagicDNS name and no mDNS candidate either. Empty targets plus remote mode yields remote-unavailable, which renders the disabled menu item at apps/web/src/components/chat/OpenInPicker.tsx:331 and disables the Open button at :297:

<MenuItem disabled>No SSH route to {environmentLabel}</MenuItem>

environmentLabel is the machine's ComputerName, hence "MBP14".

The worktree correlation is a red herring. useRemoteOpenState is keyed on the environment, not the thread or its cwd, so a worktree thread and a main-branch thread on the same environment resolve identically. projectScriptCwd correctly returns the worktree path. The user's worktree threads simply happened to be the recent ones, opened from the LAN-IP tab, while older main-branch threads were used from a loopback tab.

Why the URL was the LAN one. Note that the default serve path is not at fault: startupPresentation defaults to "browser" (apps/server/src/cli/config.ts:294), and resolveStartupBrowserTarget (apps/server/src/serverRuntimeStartup.ts:257-261) resolves a wildcard host to http://localhost:${port}, so the auto-opened tab is already correct. The LAN address is what resolveHeadlessConnectionString (apps/server/src/startupAccess.ts:45-78) picks for headless output and for t3 pair (apps/server/src/cli/pair.ts:135) — appropriate there, since that URL is meant for a phone. A user who pairs a mobile device and then keeps or bookmarks that URL on the desktop lands in this state permanently.

Suggested direction: when the environment is a primary target and the URL host matches one of this machine's own non-internal interface addresses, treat it as local-exec rather than remote. Failing that, remote-unavailable should fall back to offering local exec instead of disabling the control outright, since the server is by definition reachable and able to spawn an editor.

Steps to reproduce

On a machine with no sshd listening on port 22 (macOS with Remote Login off) and Tailscale absent:

  1. Start the server bound to all interfaces: npx t3@nightly serve --host 0.0.0.0
  2. In a browser on that same machine, open the server by its LAN address rather than localhost, e.g. http://192.168.0.112:3773. (Pairing a phone and reusing the pairing URL on the desktop gets you here naturally; so does bookmarking it.)
  3. Open any thread in a project.
  4. Click the Open-in-editor dropdown in the chat header.

Expected: VS Code opens the thread's cwd on this machine, as it does when the same server is reached at http://localhost:3773.

Actual: the menu shows a disabled "No SSH route to " item and the Open button is disabled. Opening the identical server at http://localhost:3773 in the same browser makes it work immediately.

Not worktree-specific — reproduces on main-branch threads too.

Version

0.0.34-nightly.20260819.1133 (commit 24c4ba68f536)

Environment

macOS 15 (darwin arm64 24.6.0), Node v24.18.0, Firefox, launched via npx t3@nightly. No sshd on port 22; Tailscale not installed.

Evidence
# Server bound to all interfaces (server.trace.ndjson)
"attributes":{"server.mode":"web","server.port":3773,"server.host":"0.0.0.0","startup.phase":"reactors.start"}

# The active browser session's client IP is this machine's own LAN address
# (auth_sessions, credential columns omitted)
method = browser-session-cookie
client_device_type = desktop
client_os = macOS
client_browser = Firefox
ip = 192.168.0.112
last_connected_at = 2026-08-19T10:08:48

# Same address is this machine's en0
$ ipconfig getifaddr en0
192.168.0.112

# No sshd, so RemoteOpenTargets advertises nothing
$ netstat -an -p tcp | grep -E '\.22 .*LISTEN'
(no output)
$ nc -z -G 2 127.0.0.1 22 ; echo $?
1
$ nc -z -G 2 ::1 22 ; echo $?
1

# Affected threads were in worktrees, but the environment-level state is what decides
# (projection_threads, home directory redacted)
branch                                  worktree_path
t3code/implement-next-interface-ticket  ~/.t3/worktrees/proj/t3code-40e3cce0
t3code/record-from-its-own-checkout     ~/.t3/worktrees/proj/t3code-ceb89d6f
Related issues

Searched open and closed issues for "SSH route", "remote open", "open in editor", "loopback", "vscode-remote", "worktree" — no match. #4915 (T3 Connect reports "relay offline" when the server binds a non-loopback host) and #4462 (--tailscale-serve leaves headless backend classified as local-only) are the same family of local-vs-remote misclassification but concern relay and Tailscale classification, not open-in-editor.

Fix applied or workaround

Nothing was written to the machine; no database writes and no source patches. The workaround is to open the same server at http://localhost:3773 on the machine itself, which flips the mode to local-exec and makes Open-in-editor work for worktree and main-branch threads alike. --host 0.0.0.0 can stay as-is, since it also serves loopback, so the phone keeps working via the LAN address.

Filed by

claude (opus-5) via t3 triage

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with resolveRemoteOpenState in apps/web/src/remoteOpen.ts and the LOOPBACK_HOSTNAMES definition in apps/web/src/environments/primary/target.ts, then trace RemoteOpenTargets.resolveTargets in apps/server/src/environment/RemoteOpenTargets.ts. Confirm the same-machine LAN-IP case chooses local execution without breaking genuinely remote targets, and verify that Open-in-editor remains usable when no SSH route is advertised.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
developer-experience, networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.