Claude Code and tmux: surviving SSH disconnects
Claude Code dies with your SSH connection because it is a foreground process on a doomed terminal. tmux fixes that in one command. Here is the exact setup, plus the three things that quietly break inside tmux and how to fix each one.
You SSH into your dev box, start a long Claude Code turn, close your laptop, and when you reconnect the session is gone. Nothing crashed. When an SSH connection drops, the shell it spawned gets SIGHUP and takes its children with it, and Claude Code was one of them. The agent did not survive because nothing about a plain SSH session is built to survive.
The fix is tmux, and for this specific use case you need about four commands, not a tmux tutorial. This post is those commands, plus the part most guides skip: what quietly breaks when Claude Code lives inside tmux, and how to fix each piece.
The four commands that matter
On the remote machine, start a named session and run Claude Code inside it:
tmux new -s claude
claude
Now the terminal you see is owned by the tmux server on the remote box, not by your SSH connection. When the connection dies, for any reason, the session keeps running: Claude keeps working, output keeps accumulating, permission prompts keep waiting patiently.
To leave on purpose, detach with Ctrl-b then d. To come back after reconnecting:
tmux attach -t claude
And the one-liner worth putting in your shell config, which connects and attaches (or creates) in one move:
ssh -t devbox "tmux new -As claude"
The -A means "attach if it exists, create if it does not", so this same command is your entry point every time. That is genuinely the whole fix for "Claude Code stops when I disconnect from SSH". A dropped Wi-Fi connection, a closed lid, a dead VPN: none of them touch the agent anymore.
Two quality-of-life settings while you are in there. tmux set -g mouse on makes scrolling Claude Code's output work the way you expect. And if you use iTerm2, tmux -CC new -As claude runs tmux in control mode, which gives you native macOS windows and scrollback instead of terminal-inside-terminal.
Why tmux and not the alternatives: nohup is for non-interactive jobs, and Claude Code is an interactive TUI that needs a live terminal to render into and take input from. mosh keeps a connection alive through roaming and sleep, which is nice, but it is solving the connection, not the session; most people who go down this road end up running mosh into tmux anyway. tmux alone is the part that actually preserves the agent.
What breaks inside tmux, part 1: "which window is this?"
Here is the part we learned building tooling on top of Claude Code rather than reading tmux docs.
Any integration that wants to find the terminal window that owns your agent (to focus it when a turn finishes, for example) normally walks up the process tree from the hook's parent process. Inside tmux, that walk silently fails, because a pane's processes are children of the tmux server, and the server's parent is the init process, not your terminal app:
25700 25699 -zsh <- the shell where claude runs
25699 1 tmux <- the tmux server. parent: pid 1
The thing attached to your terminal is the tmux client, a different process entirely. So the ancestry chain from Claude Code dead-ends at the server, and any window-raising logic based on it points nowhere. The fix is to stop climbing and use what tmux gives you: the $TMUX and $TMUX_PANE environment variables are inherited by everything in the pane, including Claude Code's hooks, and from a pane id you can resolve pane, then session, then the attached client, and walk up from the client's pid instead. If you use hooks for anything window-related, pass $TMUX_PANE along.
What breaks, part 2: your hooks fire on the wrong machine
If you followed a notification guide and your hooks do nothing over SSH, that is not a tmux problem, but you will meet it at the same time. Hooks run where the claude process runs, which is the remote box, so your notification lands on a machine you are not looking at. The fix is an SSH reverse tunnel so the remote hooks can reach your local machine, and we wrote that up separately in Why your Claude Code hooks do nothing over SSH. One extra trap from that post applies doubly here: process ids and $TMUX_PANE values from the remote machine mean nothing on your Mac, so remote hooks should mark themselves remote and let the receiving side skip anything machine-local.
What breaks, part 3: knowing when anything finished
tmux makes it painless to run several Claude Code sessions at once, one per project, and that is exactly when you stop noticing which one needs you. A detached session that hit a permission prompt will wait forever without a sound. Options, in increasing order of effort:
- tmux's built-in
monitor-activityandmonitor-silenceflags will highlight a window when output changes or stops. Crude but zero setup. - A
StopandNotificationhook that fires a notification with the project name, which is a five-minute setup we covered in the hooks notification guide. - The reason this blog exists: Unwait listens to those hooks and shows which session finished or got blocked, with one click to focus the right window, and fills the wait with a flashcard while you are at it. It exists because I was running three tmux sessions and reading none of them.
The checklist
ssh -t devbox "tmux new -As claude"as your standard entry. The session survives every disconnect.Ctrl-b dto leave on purpose;tmux attach -t claudeto return.set -g mouse on, and iTerm2 users get native windows withtmux -CC.- Tooling that finds windows by walking the process tree breaks inside tmux; use
$TMUX_PANEinstead. - Hooks run on the machine where claude runs. Remote agent means remote hooks, which means a reverse tunnel if the notification should reach you.
- If you run more than one session, set up completion and blocked alerts before you need them. A patient agent waiting on a permission prompt is the quietest way to lose an afternoon.