# Keep Claude Code sessions running: implementation spec for an AI agent

Source: https://jacobkunselman.com/tips/claude-code-sessions/ (AI tips, No. 1, 2026-09-25)

You are an AI coding agent. Your user wants their Claude Code sessions to survive reboots
and updates without retyping anything. Implement the system below on the user's machine,
adapting paths, the terminal emulator and the package manager to what you find there. A
working reference implementation (Raspberry Pi OS, Debian-based, systemd, labwc desktop) is
in `reference/` next to this file. Read it; don't copy it blindly.

Built and verified with Claude Code 2.1.282. Anything marked *undocumented* is Claude Code
internals that were observed, not promised; check that it still holds on the installed version.

## Goal

- One Claude Code session per project folder, each in its own tmux window.
- All of them start at login. Closing the terminal doesn't end them.
- One command (and a nightly timer) updates the OS and Claude Code, then restarts each IDLE
  session on the new version. A busy session is never interrupted.
- A restart can resume the conversation instead of starting fresh.
- Each session is named after its folder.

## Components

1. **`sessions.conf`**: one project folder per line (`~` allowed, `#` comments). It is the
   list started at login. The tmux window name is the folder's basename.
2. **`claude-sessions.py`**, with subcommands:
   - `start [NAME…] [--open]`: create any missing window (all of `sessions.conf`, or just the
     NAMEs). `--open` also opens a terminal attached to tmux.
   - `stop NAME…|--all [--force]`: SIGTERM Claude, wait up to 20 s, close the window. Idle
     sessions only, unless `--force`.
   - `restart [NAME…] [--outdated] [--min-idle MIN]`: restart idle sessions, optionally only
     those older than the installed version.
   - `continue NAME [--session ID] MESSAGE`: a one-shot note. The next start of NAME runs
     `claude --resume ID -n NAME "MESSAGE"` instead of a fresh `claude`, then deletes the note.
   - `list`, `status`, `autostart NAME on|off`.
3. **`tmux.conf`**, for the private server only.
4. **`update.sh [--nightly]`**: OS upgrade, `claude update`, reboot only if needed, otherwise
   `restart --outdated`, then `start --open`.
5. **Login autostart**: an XDG autostart `.desktop` entry, or the desktop's own autostart,
   running `claude-sessions.py start --open`.
6. **A nightly systemd user timer** running `update.sh --nightly`, plus an optional taskbar
   launcher for `update.sh`.

## Key commands

```sh
tmux -L claude new-session -d -s claude -n NAME -c DIR "bash -ic 'claude -n NAME; exec bash -i'"
tmux -L claude new-window  -d -t claude: -n NAME -c DIR "bash -ic 'claude -n NAME; exec bash -i'"
tmux -L claude respawn-pane -k -t WINDOW_ID -c DIR "…same command…"
tmux -L claude list-panes -s -t claude -F '#{window_name}\t#{window_id}\t#{pane_pid}\t#{pane_current_command}\t#{pane_start_path}'
claude --resume SESSION_ID -n NAME "message"      # resume and send a first message
claude update                                      # native installer; exits when done
```

- `-L claude` is a private tmux server, so other tmux use can't collide with it.
- `bash -ic '…; exec bash -i'` gives the user's normal PATH and aliases, and leaves a shell
  in the folder if Claude exits.
- `-n NAME` sets the session name that the mobile app and `/resume` show. Without it you get
  `folder-3f`, or a topic Claude picks. `/rename NAME` renames a running session.

## Detecting state (undocumented)

- **Installed version:** the native installer keeps `~/.local/bin/claude` as a symlink to
  `~/.local/share/claude/versions/<version>`. The basename of its target is the latest version.
  It downloads new versions by itself; running sessions keep the old binary until restarted.
- **Per-session state:** `~/.claude/sessions/<pid>.json`, with `pid`, `sessionId`, `cwd`,
  `kind` ("interactive" for terminal sessions), `status` ("idle", "busy", …), `version`,
  `statusUpdatedAt` (ms) and `procStart`. The files can be stale: accept one only if
  `procStart` equals field 22 of `/proc/<pid>/stat` (guards against pid reuse).
- **Which window a session is in:** walk the process's parents up to a tmux `pane_pid`.
- **The transcript:** `~/.claude/projects/<cwd with "/" and "." replaced by "-">/<sessionId>.jsonl`.

## Rules that make it safe

1. **Idle means BOTH:** `status == "idle"` AND the transcript unwritten for 60 s. The status
   flag has been seen reading "idle" in the middle of a turn. Anything not idle is skipped and
   reported, never killed. That includes sessions waiting on a permission prompt.
2. **Stop gracefully:** SIGTERM the `claude` process, wait for it to exit (Claude saves the
   transcript), then `respawn-pane -k`. If it hasn't exited after 20 s, leave it alone.
3. **Strip the environment.** Before starting the tmux server, remove every variable starting
   with `CLAUDE` (`CLAUDECODE`, `CLAUDE_CODE_*`, …) plus `TMUX` and `TMUX_PANE`. Launched from
   inside a Claude session, the server otherwise hands those to every new session, and each
   thinks it is a child of the caller.
4. **Detach from the caller's cgroup.** Start the tmux server (the first `new-session`) and the
   terminal window with `systemd-run --user --scope --quiet --collect …`. When a oneshot
   systemd service (the nightly timer) exits, systemd kills everything left in its cgroup, and
   that includes a tmux server it started. An agent's tool call can reap its process group the
   same way.
5. **Pin the display for headless starts.** Started from a timer or over ssh, set
   `WAYLAND_DISPLAY`/`DISPLAY`/`DBUS_SESSION_BUS_ADDRESS` from `$XDG_RUNTIME_DIR` if the sockets
   exist, so clipboard and URL opening work in the sessions.
6. **Wait for the network at boot:** poll DNS for `api.anthropic.com` for up to 2 minutes before
   the first start.
7. **Reboot only when needed:** `/run/reboot-required` exists, or the newest kernel in
   `/lib/modules` for the running flavour isn't `uname -r`, or libc/systemd/dbus/firmware
   packages were upgraded (set a marker in `$XDG_RUNTIME_DIR`, which a reboot clears). When run
   interactively, offer the reboot with a countdown. At night, reboot only if the user opted in
   and every session has been idle 30 minutes or more.
8. **Non-interactive apt:** `DEBIAN_FRONTEND=noninteractive`, `-o DPkg::Lock::Timeout=600`,
   `-o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold`. Log the full output.
9. **Background sessions:** a conversation can move into Claude's background daemon
   (`claude daemon run`). Its workers are processes whose executable is under
   `~/.local/share/claude/versions/`, and whose process name is the version number, not
   `claude`. To stop "all Claude", match on `/proc/*/exe`, not the process name.

## tmux settings

```
set -s escape-time 10     # Esc is otherwise delayed 500 ms and Claude feels broken
set -g mouse on           # click a window name to switch; Shift+drag for the terminal's own selection
set -g history-limit 50000
set -g base-index 1
```

`Ctrl-b` is both tmux's prefix and a Claude Code shortcut. Press it twice for Claude, or bind
another prefix.

## Acceptance tests (run them all)

1. Reboot: every folder in `sessions.conf` has a tmux window running `claude -n <folder>`, and
   a terminal is attached.
2. Close the terminal window: `tmux -L claude ls` still lists the session. `start --open`
   reattaches.
3. Give one session a long task and run `restart`: that session is reported busy and left
   alone, and the idle ones restart.
4. Run `start` from inside a Claude session with no tmux server: no new session has
   `CLAUDECODE` in `/proc/<pid>/environ`.
5. Run `start` from a oneshot systemd user service with no tmux server: the sessions are still
   running after the service exits.
6. `continue NAME --session <id> "hello"`, then `restart NAME`: the conversation comes back and
   receives "hello". The note is gone afterwards.
7. `update.sh` with nothing to update: nothing restarts, and the summary says so.

Test the logic against a stub first. Point the command at a script that writes a fake
session JSON and sleeps, on a separate `tmux -L claude-test` server, before touching the
user's real sessions.
