tmux-assistant-resurrect persists AI coding assistant sessions (Claude Code, OpenCode, Codex CLI) across tmux restarts. It hooks into tmux-resurrect to save session IDs and restore them automatically.
tmux-assistant-resurrect.tmux-- TPM plugin entry point (sets tmux options, installs hooks)hooks/-- Native hooks/plugins for each assistant tool (write session IDs to state files)scripts/lib-detect.sh-- Shared library:detect_tool(),pane_has_assistant(),posix_quote()scripts/save-assistant-sessions.sh-- Resurrect post-save hook (process detection + session IDs + enriched fields viaextract_cli_args())scripts/restore-assistant-sessions.sh-- Resurrect post-restore hook (resumes assistants with CLI flags + env vars)config/-- tmux configuration snippet (used byjust install, not TPM)docs/design-principles.md-- Detection approach, session ID extraction, process title behaviorjustfile-- Developer recipes (install, uninstall, status, test); end users use TPMtest/-- Docker-based integration tests with real CLI binaries
- No wrapper scripts: Do not create wrapper functions/aliases around
claude,opencode, orcodex. Use native hook/plugin systems instead. - Restore hook is the sole launcher: Assistants must NOT be listed in
@resurrect-processes. The post-restore hook handles all resuming with correct session IDs. Adding them to@resurrect-processescauses double-launch. - TPM-only installation for end users: Users install via TPM (
set -g @plugin 'timvw/tmux-assistant-resurrect'+prefix + I). Thejustfilerecipes are for developers only. - Pipe delimiter in tmux format output: tmux 3.4 converts tabs and control
characters in
-Foutput. Use|as delimiter (documented limitation: paths containing|will break, but|is extremely rare in directory names). - Two-guard restore: The restore script has two independent guards before injecting a resume command into a pane: (1) the pane's foreground process must be a known shell, and (2) the pane must not already have a running assistant in its process tree. Both must pass. This prevents typing into TUIs or double-launching.
- Restore shell whitelist: Guard 1 strips a leading
-(login shells report as-bash,-zsh, etc.) then checks against a hardcoded whitelist:bash,zsh,fish,sh,dash,ksh,tcsh,csh,nu. If a user's shell isn't in this list, restore silently skips the pane. Update the whitelist inscripts/restore-assistant-sessions.shif needed.
Agent detection uses direct process inspection: the save script takes a single
ps -eo pid=,ppid=,args= snapshot and matches child processes of tmux pane
shells against known assistant binary names via detect_tool() in
scripts/lib-detect.sh.
Session ID extraction uses tool-native mechanisms (state files, process args,
JSONL lookup, SQLite database) -- this is infrastructure plumbing, not heuristic
classification. Both Claude and OpenCode overwrite their process titles, but
on macOS arm64 (v2.1.44+) process args are still visible via ps -eo args=.
State files and database queries remain the primary extraction methods, with
process args as a reliable fallback.
- All scripts use
set -euo pipefail - State files go to
$TMUX_ASSISTANT_RESURRECT_DIR(default:$XDG_RUNTIME_DIRor$TMPDIR+/tmux-assistant-resurrect) - State files contain the full tool-provided context (merged from hook stdin /
plugin events) plus plugin metadata (
tool,ppid/pid,timestamp,env). The Claude hook merges Claude's entire SessionStart JSON; the OpenCode plugin captures the full Session object. The save script readssession_id,model, andenvfrom state files andcli_argsfrompsprocess args. The restore script usescli_argsto reconstruct the original CLI invocation and restores user-configured env vars (from@assistant-resurrect-capture-env) as a command prefix. - The
envobject in state files capturesTMUX_PANEandSHELLby default, plus user-configured vars via@assistant-resurrect-capture-envtmux option (space-separated list, set in tmux.conf) - Log files go to
~/.tmux/resurrect/assistant-{save,restore}.log(truncated to 500 lines per run) - Process inspection uses
ps -eo pid=,ppid=(notpgrep -P-- unreliable on macOS) - Agent detection matches binary names via
casepatterns indetect_tool() - Hook install uses two-phase matching: exact equality (
== $cmd) to detect whether the current-path hook is already installed, and substring match (contains("claude-session-track")) to clean up stale copies left by path changes (e.g., Nix rebuilds). Cleanup runs when the current hook is missing OR stale copies exist. The// ""null-coalescing on.commandprevents crashes on hook entries that lack a.commandfield (e.g., URL-type hooks), and.hooksis null-coalesced before mapping to handle entries with missing/null hooks arrays - Use
posix_quote()fromlib-detect.shfor any values sent to tmux panes viasend-keys(safe for bash, zsh, fish, and other POSIX-ish shells) - Hook command paths use single quotes (
bash '${CURRENT_DIR}/hooks/...'); this breaks if the install path contains a single quote (unlikely with TPM) - The sidecar JSON (
assistant-sessions.json) entries include enriched fields:model(from state file or--modelin args),cli_args(frompsargs with binary name and session/resume args stripped),env(from state file). All are optional for backward compatibility. extract_cli_args()insave-assistant-sessions.shstrips per-tool session args: Claude--resume[= ]<id>, OpenCode--session[= ]<id>and-s <id>, Codexresume <id>. Returns normalized whitespace-trimmed string.- The restore script only restores env vars listed in
@assistant-resurrect-capture-env(nottmux_paneorshell), prepended asVAR='val'prefix to the resume command
These assumptions were derived from reading upstream source code. If behavior changes after an upgrade, check the relevant source to confirm.
| Assumption | Why it matters | Where to verify |
|---|---|---|
Claude sets process.title = 'claude' |
Node.js sets the process title, but on macOS arm64 (v2.1.44) ps -eo args= still shows full args (e.g., claude --dangerously-skip-permissions). The save script's extract_cli_args() relies on this. If a future version hides args, cli_args will be empty and restore falls back to bare <binary> <resume_arg>. |
Run ps -eo args= on a running Claude process; Claude Code source: search for process.title |
Claude hook spawns intermediate sh -c |
$PPID in the hook is NOT Claude's PID; hooks walk the process tree via find_claude_pid() (max 5 levels) |
Run ps -eo pid=,ppid=,args= while a hook is executing |
| OpenCode plugins run in-process | process.pid in the plugin IS the opencode binary's PID; state file is keyed by this PID |
OpenCode source: search for await import( in the plugin loader (approx. packages/opencode/src/plugin/index.ts -- path may move) |
| OpenCode Go binary overwrites process title | -s <id> is NOT visible in ps; plugin state file or SQLite DB are the reliable sources |
Run ps -eo args= on a running opencode -s <id> process |
OpenCode SQLite DB at ~/.local/share/opencode/opencode.db |
Fallback session ID extraction when plugin state file and args are unavailable; matches by cwd + most recent time_updated |
Check DB schema: sqlite3 ~/.local/share/opencode/opencode.db ".schema session" |
Codex writes ~/.codex/session-tags.jsonl |
Primary session ID source for Codex (PID → session mapping) | Run Codex and check cat ~/.codex/session-tags.jsonl |
tmux-resurrect pane content archive layout: ./pane_contents/pane-{session}:{window}.{pane} inside pane_contents.tar.gz |
strip_assistant_pane_contents() removes assistant pane files from this archive to prevent stale TUI flash on restore |
tmux-resurrect source: scripts/helpers.sh:pane_contents_file() |
These are hard-won lessons. Do not "simplify" them away.
| Gotcha | Details |
|---|---|
macOS pgrep -P is unreliable |
Silently misses child processes. Always use ps -eo pid=,ppid= with awk |
| tmux 3.4 mangles delimiters | Converts tabs to underscores, control characters to octal escapes in -F output. Use ` |
printf %q breaks fish shell |
Not POSIX. Use posix_quote() (single-quote wrapping with '\'' escaping) instead |
|| continue inside $() runs in the subshell |
continue executes but only affects the subshell, not the outer loop. Place || continue outside the $() |
kill -0 0 succeeds |
Checks current process group, not PID 0. Always validate PIDs are numeric and > 1 before kill -0 |
| npx wrapper chains | npx opencode spawns npm → sh → node → opencode (4+ levels). Use wait_for_descendant() (full tree walk) not wait_for_child() (direct children only) |
tmux-resurrect execute_hook() uses eval |
Hook stdout goes to the active pane. Log to stderr only |
process.title vs ps args |
Claude Code sets process.title = 'claude' (Node.js), but ps -eo args= still shows full command line on macOS arm64 v2.1.44. This may not hold on Linux or future versions. extract_cli_args() degrades gracefully to empty string |
Claude permission_mode not in SessionStart hooks |
Claude Code v2.1.44 passes undefined for permission_mode in executeSessionStartHooks. The save script works around this by extracting --dangerously-skip-permissions from ps args via extract_cli_args() |
Tests run in Docker with real CLI binaries (@anthropic-ai/claude-code,
opencode-ai, @openai/codex). No mocks, no API keys needed.
# Run the full test suite in Docker
just test
# Manual debugging on a live system
just save # trigger a save manually
just status # check installation status
just clean # remove stale state files
cat ~/.tmux/resurrect/assistant-sessions.json | jq .
cat ~/.tmux/resurrect/assistant-save.log
cat ~/.tmux/resurrect/assistant-restore.log- The save script has a
main()guard so tests cansourceit to call extraction functions directly without executing the full save flow. - Tests use polling helpers (
wait_for_child,wait_for_descendant,wait_for_death) instead of fixedsleep-- fast on fast machines, tolerant on slow CI. kill_pane_children()does tree-walk cleanup instead of inline kill patterns.- npm packages are pinned to major versions:
claude-code@^2,codex@^0,opencode-ai@^1.
- Add a
casepattern indetect_tool()inscripts/lib-detect.sh - Add a
get_<tool>_session()function inscripts/save-assistant-sessions.sh - Add a restore command in
scripts/restore-assistant-sessions.sh - Optionally add a hook/plugin in
hooks/if the tool doesn't expose session IDs externally - Update install/uninstall recipes in
justfileandtmux-assistant-resurrect.tmuxif a new hook was added - Add tests in
test/run-tests.sh
Use Conventional Commits:
feat: add support for <tool>fix: handle <edge case>docs: update README