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.

I was 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 actual 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-control

The phone ran this command over SSH:

Terminal window
wsl -d Ubuntu -- bash /mnt/g/Projects/workspace/admin/rc_spawn.sh

rc_spawn.sh created a detached tmux session, started claude.exe --remote-control, then captured the pane to pull out the join URL. The script was not failing before launch. It created a visible session, returned a real URL, and made the phone look successful. That made the failure easy to blame on the relay, the phone connection, or Remote Control itself.

A registered session is not a live process

The diagnostic path started with the symptom that mattered: the failure followed the launcher, not the phone.

The URL was printed before the session died. The phone could open it. A message from the app did not receive an ordinary error, it just spun. 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 because 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 Windows executable remains tied to the wsl.exe client that launched it. In this case, tmux owned a Linux shell, but 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, not after adding retries or another health check.

The rejected fixes were all attached to the wrong boundary

I considered three simpler repairs before changing the chain.

The first was to keep the direct tmux -> claude.exe design and make the WSL side more persistent. That lost because the problem was not whether the Linux multiplexer survived. It was that the Windows process was an interop child. A longer-lived tmux session cannot adopt a process that Windows tears down with its WSL launcher.

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 not necessary 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. The important relationship is the one 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 ConPTY

tmux 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 no longer a Windows executable launched through WSL interop. It is a native Windows process launched by Windows SSH, with an interactive terminal and a parent connection whose lifetime tmux owns.

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 I need to inspect or clean one up, tmux ls and tmux kill-session -t <name> are the control surface.

The important correction was not “use SSH instead of WSL.” WSL is still in the path, and tmux is still doing useful work. The correction was to stop asking WSL to be the parent of a Windows interactive process that had to outlive its launcher.

That distinction is now a hard rule in our cross-platform automation: when the child must outlive the handoff, make sure the component that owns the child actually lives on the child’s operating system.