Disclaimer: This project was entirely vibecoded (designed and implemented through conversation with AI coding assistants). It has been end-to-end tested in Docker with real CLI binaries (170+ automated tests + full save/kill/restore lifecycle smoke test), but has limited real-world usage so far. Expect rough edges. Contributions and bug reports welcome.
Persist and restore AI coding assistant sessions across tmux restarts and reboots.
When your computer shuts down, tmux sessions are lost -- including any running Claude Code, OpenCode, or Codex CLI instances. This project hooks into tmux-resurrect to automatically save assistant session IDs, CLI flags, and environment variables, then re-launch them with the exact same configuration after a restore.
SAVE (every 5 min + manual prefix+Ctrl-s)
tmux-resurrect saves pane layouts
-> post-save hook inspects child processes of each pane
-> detects assistants by binary name (claude, opencode, codex)
-> extracts session IDs via native hooks/plugins/process args
-> writes ~/.tmux/resurrect/assistant-sessions.json
RESTORE (on tmux start or manual prefix+Ctrl-r)
tmux-resurrect restores pane layouts
-> post-restore hook reads assistant-sessions.json
-> reconstructs full CLI invocation with saved flags + env vars
-> sends resume commands to each pane, e.g.:
ANTHROPIC_BASE_URL='...' claude --dangerously-skip-permissions --resume <id>
opencode --verbose -s <session-id>
codex --full-auto resume <session-id>
Detection is done via direct process inspection: the save script takes a
single ps snapshot of all processes, finds children of each tmux pane shell,
and matches known assistant binary names (claude, opencode, codex).
Session ID extraction uses tool-native mechanisms (infrastructure plumbing):
| Tool | Primary method | Fallback 1 | Fallback 2 | Notes |
|---|---|---|---|---|
| Claude Code | SessionStart hook state file (keyed by Claude PID) |
--resume in process args |
- | Claude overwrites its process title, so args fallback only works if args are visible |
| OpenCode | -s / --session in process args |
Plugin state file | SQLite DB query (~/.local/share/opencode/opencode.db) |
Go binary overwrites process title; DB fallback matches most recent session by cwd |
| Codex CLI | PID lookup in ~/.codex/session-tags.jsonl |
resume in process args |
- | Codex runs via Node.js, so args are always visible in ps |
Each tool has a primary and fallback extraction method. Fallbacks address the chicken-and-egg problem: after a restore, session IDs are in process args even before hooks/plugins have fired. The OpenCode SQLite database fallback provides version-resilient session ID extraction even when the plugin hasn't fired.
- tmux (tested with 3.x)
- TPM (Tmux Plugin Manager)
- jq (used by save/restore scripts)
- At least one of: Claude Code, OpenCode, Codex CLI
Install TPM if you don't have it:
git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpmAdd to your ~/.tmux.conf:
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-resurrect'
set -g @plugin 'tmux-plugins/tmux-continuum'
set -g @plugin 'timvw/tmux-assistant-resurrect'
# Optional: restore terminal text in non-assistant panes after tmux restart.
# If enabled, the plugin automatically strips captured content for assistant
# panes so restore won't briefly flash stale TUI output before resuming.
# set -g @resurrect-capture-pane-contents 'on'
# Initialize TPM (must be last line)
run '~/.tmux/plugins/tpm/tpm'Then inside tmux, press prefix + I (capital I). TPM will clone the plugins
and automatically set up:
- tmux-resurrect + tmux-continuum settings
- Claude Code hooks in
~/.claude/settings.json - OpenCode session-tracker plugin in
~/.config/opencode/plugins/
Remove the @plugin 'timvw/tmux-assistant-resurrect' line from ~/.tmux.conf,
then press prefix + alt + u inside tmux.
Once installed, everything runs automatically:
- tmux-continuum saves your tmux layout every 5 minutes
- Post-save hook collects assistant session IDs at each save
- On tmux server start, continuum auto-restores the layout
- Post-restore hook resumes each assistant with its saved session ID
Manual save/restore keybindings (tmux-resurrect defaults):
| Key | Action |
|---|---|
prefix + Ctrl-s |
Save tmux state + assistant sessions |
prefix + Ctrl-r |
Restore tmux state + resume assistants |
tmux-assistant-resurrect.tmux # TPM plugin entry point
config/
resurrect-assistants.conf # tmux config reference template (not sourced automatically)
hooks/
lib-claude-pid.sh # Shared helper: walks process tree to find Claude PID
claude-session-track.sh # Claude SessionStart hook (writes session ID)
claude-session-cleanup.sh # Claude SessionEnd hook (removes state file)
opencode-session-track.js # OpenCode plugin (tracks session ID + cleanup)
scripts/
lib-detect.sh # Shared library (detect_tool, pane_has_assistant, posix_quote)
save-assistant-sessions.sh # Resurrect post-save hook (process detection + session IDs)
restore-assistant-sessions.sh # Resurrect post-restore hook (resumes assistants)
test/
Dockerfile # Docker image with tmux, jq, just, and real assistant CLIs
run-tests.sh # Integration test suite
justfile # Install/uninstall/status/save/restore/test recipes
The full test suite runs in Docker with real CLI binaries (no mocks):
just testThis builds a Docker image with tmux, jq, just, and the real
@anthropic-ai/claude-code, opencode-ai, and @openai/codex npm packages,
then runs the full test suite covering install, save, restore, uninstall, hooks,
cleanup, TPM plugin installation, session ID extraction, POSIX quoting, process
tree detection, upgrade-path migration, and regression scenarios. No API keys are needed — the tests exercise
the process detection and session management layer, not the AI functionality.
You can verify the full save → kill → restore cycle on your own machine using the normal TPM installation — no cloning or build tools needed.
Prerequisites: tmux, jq, and at least one of claude / opencode / codex installed.
Follow the Installation steps above (install TPM, add the
plugin lines to ~/.tmux.conf, press prefix + I inside tmux).
Start assistants in separate tmux windows or sessions — just like you normally would:
# In one tmux window:
cd ~/src/my-project
claude
# In another window:
cd ~/src/other-project
opencodeWork with them for a bit so the session hooks fire (Claude's SessionStart
hook writes the session ID to disk automatically).
Press prefix + Ctrl-s (the tmux-resurrect save keybinding). This saves the
tmux layout and runs the assistant save hook, which detects running
assistants and writes their session IDs to
~/.tmux/resurrect/assistant-sessions.json.
You can inspect what was saved:
cat ~/.tmux/resurrect/assistant-sessions.json | jq .Example output:
{
"timestamp": "2026-02-15T20:34:28Z",
"sessions": [
{
"pane": "my-project:0.0",
"tool": "claude",
"session_id": "01abc...",
"cwd": "/home/user/src/my-project",
"pid": "12345",
"model": "claude-opus-4-6",
"cli_args": "--dangerously-skip-permissions --model claude-opus-4-6",
"env": {"tmux_pane": "%1", "shell": "/bin/zsh", "ANTHROPIC_BASE_URL": "https://proxy.internal"}
},
{
"pane": "other-project:0.0",
"tool": "opencode",
"session_id": "ses_xyz...",
"cwd": "/home/user/src/other-project",
"pid": "12346",
"model": "",
"cli_args": "",
"env": {"tmux_pane": "%2", "shell": "/bin/zsh"}
}
]
}tmux kill-serverEverything is gone — all sessions, all panes, all running assistants.
Start tmux again:
tmuxThen press prefix + Ctrl-r (the tmux-resurrect restore keybinding).
tmux-resurrect recreates your sessions, windows, and panes. The post-restore hook then reads the saved assistant sessions and sends the correct resume command to each pane, preserving the original CLI flags and environment:
claude --dangerously-skip-permissions --model opus --resume <session-id>opencode -s <session-id>ANTHROPIC_BASE_URL='...' codex resume <session-id>
If the session was launched with flags like --dangerously-skip-permissions or
--model, those flags are captured from ps at save time and replayed on
restore. Environment variables configured via @assistant-resurrect-capture-env
are prepended to the resume command.
Check the restore log to see what happened:
cat ~/.tmux/resurrect/assistant-restore.logYou should see lines like:
[2026-02-15T20:34:31Z] restoring 2 assistant session(s)...
[2026-02-15T20:34:31Z] restoring claude in my-project:0.0 (session: 01abc..., cmd: claude --dangerously-skip-permissions --resume '01abc...')
[2026-02-15T20:34:32Z] restoring opencode in other-project:0.0 (session: ses_xyz..., cmd: opencode -s 'ses_xyz...')
[2026-02-15T20:34:33Z] restored 2 of 2 assistant session(s)
The save log is also available if you want to see what was detected:
cat ~/.tmux/resurrect/assistant-save.log| Symptom | Check |
|---|---|
| Save finds 0 sessions | Run ps -eo pid=,ppid=,args= | grep -E 'claude|opencode|codex' to verify assistants are running |
| Session ID missing for Claude | Verify the hook is installed: jq '.hooks.SessionStart' ~/.claude/settings.json |
| Session ID missing for OpenCode | Launch with -s <id>, or verify the plugin: ls ~/.config/opencode/plugins/session-tracker.js |
| Restore launches but assistant says "session not found" | The session ID may have expired. This is normal — start a fresh session and the next save will pick up the new ID |
| Assistants launch twice after restore | Make sure assistants are not listed in @resurrect-processes — the plugin handles all resuming via the post-restore hook |
just test fails with Docker errors |
Ensure Docker is running and you have network access (the image pulls npm packages) |
Session tracking files are written to a per-user temporary directory:
| Platform | Default path |
|---|---|
| Linux (systemd) | $XDG_RUNTIME_DIR/tmux-assistant-resurrect (e.g., /run/user/1000/tmux-assistant-resurrect) |
| macOS | $TMPDIR/tmux-assistant-resurrect (e.g., /var/folders/.../T/tmux-assistant-resurrect) |
| Fallback | /tmp/tmux-assistant-resurrect (only if both XDG_RUNTIME_DIR and TMPDIR are unset) |
This avoids permission conflicts on multi-user systems. Override with:
export TMUX_ASSISTANT_RESURRECT_DIR=/path/to/stateNote: state files are transient — they track running assistant PIDs and session
IDs while tmux is active. The persistent sidecar JSON
(~/.tmux/resurrect/assistant-sessions.json) is what survives reboots and lives
in your home directory.
By default, the plugin captures TMUX_PANE and SHELL in each assistant's
state file. To capture additional environment variables, set a space-separated
list in tmux.conf:
set -g @assistant-resurrect-capture-env 'VIRTUAL_ENV NODE_ENV CONDA_DEFAULT_ENV'Captured variables are stored in the state file's env object and propagated
to assistant-sessions.json. On restore, variables listed in
@assistant-resurrect-capture-env are prepended to the resume command:
VIRTUAL_ENV='/home/user/.venv' claude --resume <session-id>
Built-in variables (TMUX_PANE, SHELL) are not restored — TMUX_PANE
would be stale after restore, and SHELL is already in the environment.
State files live in a user-only directory (mode 0700).
Note: Avoid capturing secrets (API keys, tokens). State files and the sidecar JSON persist to disk and may outlive the process they were captured from.
Edit config/resurrect-assistants.conf:
set -g @continuum-save-interval '5' # minutes
To add a new AI coding assistant:
- Detection: Add a
casepattern indetect_tool()inscripts/save-assistant-sessions.shmatching the tool's binary name - Session ID extraction: Add a
get_<tool>_session()function - Restore command: Add a
casebranch inscripts/restore-assistant-sessions.shwith the tool's resume command - Session tracking (optional): If the tool doesn't expose its session ID in process args or a known file, create a hook/plugin similar to the existing ones
- Update install/uninstall recipes in
justfileif a new hook was added
Two hooks configured in ~/.claude/settings.json:
SessionStart: Claude Code passes JSON on stdin (includingsession_id,model,source,permission_mode,transcript_path, and more). The hook merges the full JSON payload with plugin metadata (tool,ppid,timestamp,env) and writes it to$STATE_DIR/claude-<PID>.json. This means any new fields Claude adds in future versions are captured automatically.SessionEnd: Removes the state file when the Claude session exits, preventing stale entries.
Note: Claude Code sets process.title = 'claude', but on macOS arm64
(v2.1.44+) ps -eo args= still shows full args. The state file remains the
primary source of session IDs, with process args as a fallback. CLI flags like
--dangerously-skip-permissions are captured from ps by the save script's
extract_cli_args() function.
An OpenCode plugin that listens for session.created, session.updated, and
session.idle events. On each event, it captures the full session object
(including model, title, and other metadata) along with init-time context
(process.argv, client API surface) and writes it to
$STATE_DIR/opencode-<PID>.json. This handles the case where
a user switches sessions at runtime (via /sessions or Ctrl+x l). The plugin
also cleans up its state file on process exit (SIGINT, SIGTERM).
Codex natively writes PID-to-session mappings in
~/.codex/session-tags.jsonl. The save script reads this file directly -- no
additional hook is needed.
Runs after each tmux-resurrect save. Takes a single ps snapshot of all
processes, finds children of each tmux pane's shell, and detects assistants by
matching binary names. Then extracts session IDs using tool-specific methods
(state files, process args, JSONL lookup). Also captures:
- CLI flags (
cli_args): extracted frompsargs with the binary name and session/resume args stripped (e.g.,--dangerously-skip-permissions --model opus) - Model (
model): from state file (preferred) or--modelin args (fallback) - Environment (
env): from state file (captured by hooks/plugins)
Writes everything to ~/.tmux/resurrect/assistant-sessions.json.
Runs after each tmux-resurrect restore. Reads the sidecar JSON and reconstructs
the full CLI invocation for each assistant: <env_prefix> <binary> <cli_args> <resume_arg>. Sends the command to each pane via tmux send-keys. If enriched
fields are missing (old-format JSON), falls back to bare resume commands.
- Running state is not preserved: Assistants restart with their conversation history loaded, but any in-flight tool calls or pending operations are lost.
- First save after install (chicken-and-egg): On initial install, no session
IDs exist yet. Assistants must complete at least one session (triggering the
hooks) before their IDs can be saved. For Codex and OpenCode with
-s, this is not an issue since session IDs are visible in process args. - Claude process title: Claude Code sets
process.title = 'claude', but on macOS arm64 (v2.1.44+)ps -eo args=still shows full args. CLI flags like--dangerously-skip-permissionsare captured frompsat save time. If a future version hides args,cli_argswill be empty and restore falls back to bare resume commands. - OpenCode without plugin: If the OpenCode plugin isn't installed and the
process was started without
-s, the session ID cannot be detected. - OpenCode DB fallback (same-cwd ambiguity): When the plugin state file is
unavailable and no
-sflag was used, the save script falls back to the OpenCode SQLite database, matching sessions by working directory. If multiple sessions share the same cwd, the most recently updated one is picked — which may not be the correct one for that specific pane. - Process inspection on macOS: Uses
ps -eo pid=,ppid=instead ofpgrep -Pdue to reliability issues withpgrepon macOS. - Pane matching after restore: tmux-resurrect preserves pane indices, so the
restore hook targets the same
session:window.paneaddresses. If you manually rearrange panes between save and restore, the mapping may be wrong.
MIT
