At 11:03 PM, a new remote-control session would register, print its join URL, and die a few seconds later.
That is a nasty failure shape. The phone showed a fresh session. The service had accepted it. The spawn script returned the URL it was supposed to return. Then the session changed to Disconnected, and any message posted from the phone sat behind a gray spinner forever.
We were building a one-tap path from a phone to a fresh Claude Code Remote Control session on a Windows development machine. The phone was deliberately thin. It connected by SSH, invoked one narrow script, received a join URL, and opened it. The workspace, credentials, project context, and interactive process all belonged on the PC.
The first version looked sensible:
phone -> Windows SSH -> WSL -> tmux -> claude.exe --remote-controlThe phone ran this command over SSH:
wsl -d Ubuntu -- bash /mnt/g/Projects/workspace/admin/rc_spawn.shrc_spawn.sh created a detached tmux session, started claude.exe --remote-control, then captured the pane to pull out the join URL. The script never failed before launch. It created a visible session, returned a real URL, and made the phone look successful. That made it easy to blame the relay, the phone connection, or Remote Control itself.
A registered session is not a live process
The failure followed the launcher. The phone had nothing to do with it.
The URL was printed before the session died, and the phone could open it. A message from the app got no ordinary error, only the spinner. The process had made it far enough to announce itself, then vanished. Dropped phone connectivity was an attractive theory, but it could not explain a process dying moments after a local spawn. The detached tmux session was another attractive theory, since it was supposed to outlive the SSH command that created it.
It did outlive that command. The Windows child did not.
WSL interop has a lifetime boundary that tmux does not erase. When a Linux process starts a Windows executable, the executable stays tied to the wsl.exe client that launched it. Here, tmux owned a Linux shell, and that shell had launched claude.exe across the WSL boundary. Once the launcher-side WSL client disconnected, Windows cleaned up the interop child. tmux stayed alive, the URL remained in its scrollback, and the actual Remote Control process was gone.
That explained the misleading combination: a successful registration, a valid URL, and a dead session. We confirmed the fix live from the phone after changing the process tree. Retries and an extra health check were never part of it.
The simpler repairs that hit the wrong boundary
Three simpler repairs came up before we changed the chain.
The first was to keep the direct tmux -> claude.exe design and make the WSL side more persistent. A longer-lived tmux session cannot adopt a process that Windows tears down with its WSL launcher, and the Windows process was an interop child. The Linux multiplexer surviving was never the question.
The second was to detach the Windows command with redirected standard input and output. That ran into a separate constraint. claude --remote-control needs an interactive terminal. Give it pipes, as redirected process launches do, and it falls into --print behavior or never initializes a Remote Control session. A plain SSH command without -t has the same problem. It can run a command, but it does not provide the terminal the program expects.
The third was a WSL keepalive. That treated the symptom as a distro-lifetime issue. It was unnecessary once the session had a real owner on the Windows side. In the final design, the Ubuntu distro stays up while tmux runs even with zero attached WSL clients. What matters is the relationship between the durable Linux process and the native Windows terminal.
The SSH hop is the ownership transfer
The working chain adds one hop that initially felt redundant:
tmux on Linux -> Linux SSH client -> Windows sshd -> rc_native.ps1 -> claude.exe --remote-control in a ConPTYtmux now owns a Linux SSH client and keeps that connection open. Windows SSH receives the connection, starts a native PowerShell entry point, and gives Claude Code a real ConPTY terminal. The program is a native Windows process launched by Windows SSH, with an interactive terminal and a parent connection whose lifetime tmux owns. WSL interop no longer launches it.
The spawner still does two small jobs. It creates a named tmux session such as rc-<stamp>, then uses tmux capture-pane to scrape the join URL. That is enough for the phone. The session can survive the original launcher exiting, an SSH drop, and the phone locking. If we need to inspect or clean one up, tmux ls and tmux kill-session -t <name> are the control surface.
The fix was not “use SSH instead of WSL.” WSL is still in the path, and tmux still does useful work. We stopped asking WSL to be the parent of a Windows interactive process that had to outlive its launcher.
That is now a hard rule in our cross-platform automation: when the child must outlive the handoff, the component that owns the child has to live on the child’s operating system. A session that registers and returns a valid link can still be a dead process, so we check that it is alive after the launcher exits.