---
title: "Why tmux stayed alive while claude.exe quietly vanished under WSL"
canonical: https://dxdev.com/blog/wsl-session-spawn-ssh-reroute/
datePublished: 2026-07-19
---
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:

```text
phone -> Windows SSH -> WSL -> tmux -> claude.exe --remote-control
```

The phone ran this command over SSH:

```bash
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 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](/blog/sessions-kept-dying-wrong-suspect/) 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:

```text
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 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.
