Infrastructure skill

Persistent Claude sessions

Keep Claude running while you switch devices, close your laptop, or step away. Start a session from your desktop, check in from your phone, and pick up right where you left off.

The problem

If you run Claude Code in a browser-based terminal (like Cockpit), your session lives inside a WebSocket connection. When that connection drops — browser tab closed, laptop lid shut, phone screen locked, network hiccup — everything dies. Claude stops mid-task. Your context is gone. You start over.

dashboard.yourdomain.com — terminal
$ claude
# 20 minutes into a research task...
# Browser tab closes
# Session gone. Context lost. Start over.

Cockpit compounds this with a 15-minute idle session timeout. If Claude is thinking quietly — running tools, processing files — and you're not clicking around in the Cockpit UI, your session gets terminated automatically, taking Claude with it.

The solution

tmux is a terminal multiplexer that runs on the server, not in your browser. Your Claude session runs inside tmux. The browser terminal just shows you a view of it — like a window into a room that exists independently of whether you're looking.

Without tmux

Claude process is a child of the browser terminal. Close the tab → kill the process. One device, one session, no recovery.

With tmux

Claude runs inside a server-side tmux session. Close the tab, switch devices, lose network — tmux keeps running. Reconnect and attach.

dashboard.yourdomain.com — main (tmux)
$ claude
# Claude is mid-task. Close the tab.

# Later, on your phone...
$ # .bashrc auto-attaches to the same tmux session
# Claude is still running. Exactly where you left it.
[main] 0:claude* 1:logs 2:bash 14:23 Feb 20

Setup

tmux runs on the server — the always-on machine where Claude Code lives. Your laptop, phone, or tablet is just the client connecting to it. Everything in this Setup section is configured on the server. See Installing tmux if you don't have it yet, and Connecting from different devices for the client side.

1

Configure tmux

Create ~/.tmux.conf on your server. This sets up mouse support, better scrollback, a readable status bar, and a built-in help popup at Ctrl+B H.

~/.tmux.conf
# True color support
set -g default-terminal "screen-256color"

# Mouse support — click to switch, scroll with wheel
set -g mouse on

# Start window numbering at 1
set -g base-index 1
set -g pane-base-index 1
set-option -g renumber-windows on

# More scrollback
set -g history-limit 10000

# Status bar
set -g status-style "bg=#0d1117,fg=#8b949e"
set -g status-left "#[fg=#3fb950,bold] [#S] "
set -g status-right "#[fg=#484f58]Ctrl+B H for help #[fg=#3fb950]%H:%M #[fg=#484f58]%b %d"
set -g window-status-current-style "fg=#3fb950,bold"
set -g pane-active-border-style "fg=#3fb950"

# Ctrl+B H — help popup
bind H display-popup -E -w 62 -h 26 "cat << 'EOF'
  tmux quick reference
  ...
EOF
read -n1 -s -r -p ' press any key to close'"

Full config available in the GitHub repository.

2

Auto-attach on terminal open

Add this to ~/.bashrc (or ~/.zshrc on macOS, where zsh is the default). It attaches to (or creates) a tmux session named main whenever you open a new terminal on the server. Use whichever condition matches how you connect:

~/.bashrc / ~/.zshrc
# SSH (most common — Termius, Blink, Termux, OpenSSH, PuTTY, etc.)
# SSH_TTY is set on interactive SSH logins only, not on `ssh host "cmd"`.
if [ -z "$TMUX" ] && [ -n "$SSH_TTY" ]; then
    exec tmux new-session -A -s main
fi

# Cockpit (browser-based terminal at https://yourserver:9090)
# Use this instead if you connect through Cockpit's web UI.
if [ -z "$TMUX" ] && [ "$XDG_SESSION_TYPE" = "web" ]; then
    exec tmux new-session -A -s main
fi

Pick one — or keep both, since the conditions don't overlap. The -z "$TMUX" guard prevents nested tmux sessions when you re-attach.

3

Fix the Cockpit idle timeout (Cockpit only)

Skip this step if you connect over SSH. By default, Cockpit terminates idle web sessions after 15 minutes — exactly long enough to time out while Claude is working quietly. Disable it:

/etc/cockpit/cockpit.conf
[Session]
IdleTimeout=0

Then restart Cockpit: sudo systemctl restart cockpit.service

Installing tmux on the server

Check whether it's already there with tmux -V. You want 3.2 or newer for the display-popup help binding; older versions still work if you remove that one block from .tmux.conf.

tmux install commands by server operating system
Server OS Install command
Debian / Ubuntu / Raspberry Pi OS sudo apt update && sudo apt install tmux
Fedora / RHEL 8+ / Rocky / AlmaLinux sudo dnf install tmux
CentOS 7 / older RHEL sudo yum install tmux
Arch / Manjaro sudo pacman -S tmux
openSUSE sudo zypper install tmux
Alpine apk add tmux as root (Alpine has no sudo by default — use doas if configured)
macOS (as the server) brew install tmux (needs Homebrew)
FreeBSD sudo pkg install tmux
Windows (WSL2 Ubuntu/Debian) sudo apt install tmux inside the WSL shell

Native Windows has no tmux. tmux is POSIX-only. On a Windows server, run Claude Code and tmux inside WSL2, or use a separate Linux/macOS box as your always-on host. PowerShell and CMD can be SSH clients, but they can't host the tmux session.

macOS as a server: works fine, but enable Remote Login in System Settings → General → Sharing so you can SSH in, and disable App Nap on Terminal so the box doesn't throttle background work. A Mac mini or always-plugged-in laptop with "prevent sleep when display is off" is a reasonable host.

Connecting from different devices

Once tmux is set up on the server, anything that can open a shell on it — SSH, mosh, or Cockpit's web terminal — works as a client. The auto-attach snippet drops you straight into the running session, so the client doesn't need to know anything about tmux.

macOS

Built-in Terminal.app or iTerm2 + ssh user@server. OpenSSH ships with macOS — no install needed.

Tip: set up ~/.ssh/config with a Host alias so you can run ssh box from any folder.

Windows

Windows Terminal with the built-in OpenSSH client (ssh user@server) is the modern path. PuTTY works too. WSL gives you a Linux shell that can both SSH out and host tmux.

Linux desktop

Any terminal emulator + ssh. Same pattern as macOS. GNOME Terminal, Konsole, Alacritty, Kitty, Wezterm — all fine.

iOS / iPadOS

Blink Shell (paid, polished, mosh support) and Termius (free tier) are the two solid choices. Both handle the on-screen Ctrl key needed for tmux's Ctrl+B prefix.

Android

Termux (free, install from F-Droid — the Play Store version is unmaintained) gives you a real shell with pkg install openssh. Termius is the easiest GUI option.

ChromeOS

Enable the Linux container (Settings → Developers → Linux development environment), then sudo apt install openssh-client and use ssh like any Linux desktop. The Secure Shell extension also works without enabling Linux.

Browser (Cockpit)

Open https://yourserver:9090 and use the Terminal app. Works from any device with a browser, including phones, but apply the Cockpit timeout fix above.

Flaky networks (any device)

Install mosh on both the server and the client (apt/dnf/brew install mosh; Blink Shell has it built in), then use mosh user@server instead of ssh. Mosh survives network changes and sleep — a good companion to tmux on cellular or while moving between Wi-Fi networks.

Mosh also needs UDP ports 60000–61000 open inbound to the server — open them in your firewall (and any cloud security group) or you'll see "mosh: connect failed".

Phone keyboards and the Ctrl key: tmux's prefix is Ctrl+B, which most mobile SSH apps expose via a software toolbar above the keyboard. If you'd rather use a different prefix, add set -g prefix C-a (and unbind C-b / bind C-a send-prefix) to ~/.tmux.confCtrl+A is a common alternative that's easier on some layouts.

Key bindings

All tmux commands start with the prefix Ctrl+B — press and release it, then press the next key. Ctrl+B H shows this as a popup in your terminal.

Windows
Ctrl+B then C New window
Ctrl+B then , Rename current window
Ctrl+B then W List all windows (interactive)
Ctrl+B then 19 Switch to window by number
Ctrl+B then N / P Next / previous window
Panes (split a window)
Ctrl+B then % Split left/right
Ctrl+B then " Split top/bottom
Ctrl+B then ↑↓←→ Move between panes
Ctrl+B then Z Zoom pane to full screen (toggle)
Scroll & session
Ctrl+B then [ Enter scroll mode (use arrows/PgUp, Q to exit)
Ctrl+B then D Detach — session keeps running
Ctrl+B then H Help popup (from this config)
Ctrl+B then ? Full built-in keybinding list

Suggested window layout

Name your windows so the status bar tells you what's running at a glance. Use Ctrl+B , to rename.

[main] 1:claude*

1: claude

Your active Claude Code session. This is where you work.

[main] 2:logs*

2: logs

journalctl -f or tail -f on scheduler or app logs.

[main] 3:shell*

3: shell

Free shell for quick commands, file checks, or running builds.

Coexisting with scheduled Claude sessions

If you run automated Claude sessions (cron-based check-ins, email processing, transcript analysis), they can conflict with your interactive session — competing for CPU and trying to process the same data at the same time.

Add this function to your scheduler to detect an active interactive session and skip the automated wake:

claude-scheduler.py
def is_interactive_session_active():
    """Check if there's an active interactive Claude session in 'main' tmux."""
    result = subprocess.run(
        ['tmux', 'has-session', '-t', 'main'], capture_output=True
    )
    if result.returncode != 0:
        return False
    result = subprocess.run(
        ['tmux', 'list-panes', '-t', 'main', '-F', '#{pane_current_command}'],
        capture_output=True, text=True
    )
    return 'claude' in result.stdout.lower()

# In your run_task() function, before spawning:
if is_interactive_session_active():
    print("Skipping: interactive session active")
    return False

The pane_current_command tmux format string returns the name of the running process — no need to parse ps aux.

Getting notified when a session needs input

tmux can highlight a tab when the process in it goes quiet — a reliable signal that something has finished and is waiting for you. This uses monitor-silence, which fires after a window has produced no output for a set number of seconds.

Add these lines to your ~/.tmux.conf:

~/.tmux.conf
# Light up tabs that have gone quiet (waiting for input)
set-window-option -g monitor-silence 20
set -g visual-silence on
# Also pass terminal bell alerts through
set -g monitor-bell on
set -g visual-bell on
set -g bell-action any

# Show ~ in tab name when silent, ! on bell
set -g window-status-format " #I:#W#{?window_silence_flag, ~,}#{?window_bell_flag, !,} "

The monitor-silence 20 value is seconds. Bump it up if it fires too often during normal processing pauses.

Why silence, not activity? monitor-activity fires on every byte of output — too noisy for Claude sessions that stream continuously. monitor-silence fires only when output stops, which is a much better proxy for "waiting for you." While a session is running it outputs constantly; when it hits a prompt or finishes, it goes quiet.

Common pitfalls

Watch out

The first session after setup won't be in tmux

The .bashrc change only applies to new terminal sessions opened after saving the file. Close your current terminal tab and open a fresh one to get tmux.

Watch out

display-popup requires tmux 3.2+

Check your version with tmux -V. If you're on an older version, remove the bind H display-popup ... block from .tmux.conf and use Ctrl+B ? for help instead.

Best practice

Name your windows

Use Ctrl+B , to name each window. The status bar shows all window names at once — without names, you'll end up with a row of identical bash entries.

Best practice

tmux survives, but the server has to stay on

Sessions persist across browser disconnects, not server reboots. A Pi or always-on server works perfectly for this; a laptop that gets shut down does not.

Related skills

Claude that keeps working after you close the tab

A few config changes. Runs on any headless Linux, macOS, or BSD server with tmux. Connect from a Mac, Windows, Linux, ChromeOS, iOS, or Android client.

View on GitHub