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

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. It detects when you're in a Cockpit terminal (XDG_SESSION_TYPE=web) and immediately attaches to (or creates) a tmux session named main.

~/.bashrc
# Auto-attach to persistent tmux session in cockpit terminals.
# XDG_SESSION_TYPE=web is set by cockpit (not SSH, not console).
if [ -z "$TMUX" ] && [ "$XDG_SESSION_TYPE" = "web" ]; then
    exec tmux new-session -A -s main
fi

SSH instead of Cockpit? Use SSH_TTY instead of XDG_SESSION_TYPE. Replace the condition with [ -n "$SSH_TTY" ] — this fires only on interactive SSH logins, not on automated ssh host "command" calls.

3

Fix the Cockpit idle timeout

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

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

Three config changes. Runs on any headless Linux server with tmux and Cockpit.

View on GitHub