Skip to content

Commit 75d9a68

Browse files
claudehyoretsu
authored andcommitted
feat: restore claude/codex agent CLI sessions on restore
Add session restore strategies for the `claude` and `codex` agent CLIs: on restore a bare launch is rewritten to resume the most recent conversation for the pane (`claude --continue`, `codex resume --last`), while an explicit `--resume`/`-r` (claude) or `resume`/`fork` (codex) is preserved. Both are wired into the default process list and registered to the `session` strategy, so it works with no configuration. Adapted from upstream tmux-resurrect tmux-plugins#558 (framework-native strategy mechanism, no hooks/python/sqlite, tested) over tmux-plugins#571 (heavy SessionStart hook + python + manual settings.json) and tmux-plugins#572 (vibe-coded fuzzy SQLite matching, no tests). Resume detection also recognizes claude's short `-c`/`-r` with whole-token matching. Reviewed PRs tracked in docs/upstream-pr-review.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent eb03690 commit 75d9a68

9 files changed

Lines changed: 229 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
# Changelog
22

3-
### Unreleased
3+
### v5.1.1, 2026-06-22
4+
5+
- Restores AI agent CLI sessions. `claude` and `codex` panes now resume their
6+
most recent conversation on restore (`claude --continue`, `codex resume
7+
--last`) instead of launching empty; an explicit `--resume`/`-r` (claude) or
8+
`resume`/`fork` (codex) you already typed is preserved. Both are in the default
9+
process list, so it works with no configuration. See
10+
[restoring AI agent CLI sessions](docs/restoring_agent_sessions.md). Adapted
11+
from upstream tmux-resurrect [#558](https://github.com/tmux-plugins/tmux-resurrect/pull/558);
12+
see [`docs/upstream-pr-review.md`](docs/upstream-pr-review.md) for the
13+
comparison against [#571](https://github.com/tmux-plugins/tmux-resurrect/pull/571)
14+
and [#572](https://github.com/tmux-plugins/tmux-resurrect/pull/572).
415

516
- Saves whose content is byte-identical to a session's latest snapshot no longer
617
write a duplicate file; the existing snapshot's mtime is refreshed instead so

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ This plugin goes to great lengths to save and restore all the details from your
6969
Optional:
7070

7171
- [restoring vim and neovim sessions](docs/restoring_vim_and_neovim_sessions.md)
72+
- [restoring AI agent CLI sessions](docs/restoring_agent_sessions.md) (`claude`, `codex`)
7273
- [restoring a previously saved environment](docs/restoring_previously_saved_environment.md)
7374

7475
Requirements / dependencies: `tmux 1.9` or higher, `bash`.
@@ -143,6 +144,8 @@ You should now be able to use the plugin.
143144

144145
- [Restoring vim and neovim sessions](docs/restoring_vim_and_neovim_sessions.md)
145146
is nice if you're a vim/neovim user.
147+
- [Restoring AI agent CLI sessions](docs/restoring_agent_sessions.md) resumes the
148+
most recent `claude` / `codex` conversation per pane, enabled by default.
146149
- [Restoring pane contents](docs/restoring_pane_contents.md) is enabled by
147150
default; this doc explains how to tune or disable it.
148151

docs/restoring_agent_sessions.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Restoring AI agent CLI sessions
2+
3+
[Claude Code](https://docs.claude.com/en/docs/claude-code) (`claude`) and
4+
[Codex CLI](https://github.com/openai/codex) (`codex`) cannot be resumed by
5+
re-launching the bare process: each conversation lives behind a session id, and
6+
a fresh launch starts an empty one. `tmux-persist` restores them through its
7+
normal [restore strategy](restoring_programs.md) mechanism, so no hooks,
8+
sidecar files, or extra dependencies are involved.
9+
10+
Both are restored **by default**`claude` and `codex` are in the default
11+
process list and registered to the `session` strategy. There is nothing to
12+
configure.
13+
14+
## How it works
15+
16+
On restore, each tool's strategy rewrites the captured command into the CLI's
17+
own "resume the most recent conversation here" form:
18+
19+
| Saved command | Restored command |
20+
|---|---|
21+
| `claude` | `claude --continue` |
22+
| `claude --model opus` | `claude --model opus --continue` |
23+
| `claude --continue` / `claude -c` | unchanged |
24+
| `claude --resume <id>` / `claude -r <id>` | unchanged |
25+
| `codex` | `codex resume --last` |
26+
| `codex fix the bug` | `codex resume --last` |
27+
| `codex resume <id>` / `codex fork <id>` | unchanged |
28+
29+
`claude --continue` and `codex resume --last` ask each CLI to reopen the most
30+
recent conversation for the pane's working directory. An explicit
31+
`--resume`/`-r` (claude) or `resume`/`fork` subcommand (codex) the user already
32+
typed is always left untouched, so a deliberately pinned session wins.
33+
34+
## Disabling
35+
36+
To stop restoring one of them, override the process list without it:
37+
38+
set -g @persist-processes 'claude' # restore claude only, not codex
39+
40+
Or point its strategy elsewhere / clear it:
41+
42+
set -g @persist-strategy-codex ''
43+
44+
## Limitation: multiple panes, same directory
45+
46+
"Most recent conversation here" is keyed by working directory. If several panes
47+
ran the same agent in the *same* directory, they all resume to the single
48+
newest conversation; the others are not individually re-targeted. Pinning a
49+
pane with an explicit `claude --resume <id>` / `codex resume <id>` before saving
50+
avoids the collapse, because the explicit command is preserved verbatim.
51+
52+
Per-pane session capture would need each CLI to expose its running session id
53+
externally (so save time could record it). Until then `--continue` /
54+
`resume --last` is the dependency-free behavior that is correct for the common
55+
one-agent-per-directory case.

docs/upstream-pr-review.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Upstream PR Review (tmux-resurrect → tmux-persist)
2+
3+
Tracks which open [tmux-resurrect](https://github.com/tmux-plugins/tmux-resurrect)
4+
pull requests have been evaluated against this fork, the verdict, and what (if
5+
anything) the fork adopted. Companion to [`upstream-triage.md`](upstream-triage.md),
6+
which covers upstream *issues*.
7+
8+
| PR | Title | Reviewed | Verdict | Outcome in fork |
9+
|---|---|---|---|---|
10+
| [#558](https://github.com/tmux-plugins/tmux-resurrect/pull/558) | Add Claude Code and Codex CLI session restore | 2026-06-22 | **Adopted (base)** | Ported `claude`/`codex` `session` strategies + default-list wiring + tests. See [`restoring_agent_sessions.md`](restoring_agent_sessions.md). |
11+
| [#571](https://github.com/tmux-plugins/tmux-resurrect/pull/571) | Restore claude sessions via `--resume <id>` | 2026-06-22 | Rejected | Heavy: needs a `SessionStart` hook, `python3`, and manual `settings.json` edits for per-pane mapping; its basic mode hand-reads `~/.claude/projects/*.jsonl` instead of using `claude --continue`. Per-pane correctness noted as a [known limitation](restoring_agent_sessions.md#limitation-multiple-panes-same-directory). |
12+
| [#572](https://github.com/tmux-plugins/tmux-resurrect/pull/572) | Resurrect `copilot`, `agy`, `codex` CLIs | 2026-06-22 | Rejected | One large pre-save hook that fuzzy-matches pane text against app SQLite DBs (`session-store.db`, `state_5.sqlite`) and process logs; author-flagged "vibe-coded", no tests, fragile across CLI/DB-schema changes. Codex coverage met by #558 instead. |
13+
14+
## Why #558 over #571 / #572
15+
16+
- **Framework-native.** #558 uses the existing per-process restore-strategy
17+
mechanism (`strategies/<cmd>_session.sh` + `@persist-strategy-<cmd>`), the same
18+
path `vim`/`nvim` use. #571 and #572 bolt on out-of-band machinery (settings
19+
hooks, sqlite reads, pane-text scraping).
20+
- **No external dependencies.** No `python3`, no `sqlite3`, no per-session
21+
sidecar files, no edits to the agents' own config.
22+
- **Tested + deterministic.** Pure command rewrite, covered by
23+
[`tests/test_agent_strategies.sh`](../tests/test_agent_strategies.sh).
24+
- **Resilient.** Defers to each CLI's own resume (`claude --continue`,
25+
`codex resume --last`) rather than depending on an on-disk layout or DB schema
26+
that can change without notice.
27+
28+
### Improvements over #558 as merged
29+
30+
- Resume detection also recognizes claude's short flags `-c` / `-r`, with
31+
whole-token matching so a `--continue` inside a prompt argument can't trip it.
32+
- Wired as fork defaults via `persist.tmux` `set_default_strategies` and the
33+
`@persist-` option namespace (not the upstream `@resurrect-` names).

persist.tmux

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ set_restore_bindings() {
2424
set_default_strategies() {
2525
tmux set-option -gq "${restore_process_strategy_option}irb" "default_strategy"
2626
tmux set-option -gq "${restore_process_strategy_option}mosh-client" "default_strategy"
27+
tmux set-option -gq "${restore_process_strategy_option}claude" "session"
28+
tmux set-option -gq "${restore_process_strategy_option}codex" "session"
2729
}
2830

2931
set_script_path_options() {

scripts/variables.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ restore_path_option="@persist-restore-script-path"
99

1010
# default processes that are restored
1111
default_proc_list_option="@persist-default-processes"
12-
default_proc_list='vi vim view nvim emacs man less more tail top htop irssi weechat mutt'
12+
default_proc_list='vi vim view nvim emacs man less more tail top htop irssi weechat mutt claude codex'
1313

1414
# User defined processes that are restored
1515
# 'false' - nothing is restored

strategies/claude_session.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env bash
2+
3+
# "claude session strategy"
4+
#
5+
# Claude Code's `claude` CLI cannot be resumed by re-launching the bare process:
6+
# every conversation lives behind a session UUID. The CLI's own
7+
# `claude --continue` reopens the most recent conversation for the pane's
8+
# working directory, which is exactly what restore wants and needs no sidecar
9+
# files, hooks, or knowledge of Claude's on-disk layout.
10+
#
11+
# Behavior:
12+
# - If the saved command already resumes/continues (long or short flag),
13+
# pass it through unchanged so the explicit choice wins.
14+
# - Otherwise append `--continue`, preserving any other captured flags.
15+
16+
ORIGINAL_COMMAND="$1"
17+
DIRECTORY="$2"
18+
19+
original_command_already_resumes() {
20+
# Whole-token match so a `--continue` inside a prompt arg never trips it and
21+
# `-c` / `-r` short forms (claude's continue/resume) are caught too.
22+
[[ "$ORIGINAL_COMMAND" =~ (^|[[:space:]])(--continue|--resume|-c|-r)([[:space:]=]|$) ]]
23+
}
24+
25+
main() {
26+
if original_command_already_resumes; then
27+
echo "$ORIGINAL_COMMAND"
28+
else
29+
echo "$ORIGINAL_COMMAND --continue"
30+
fi
31+
}
32+
main

strategies/codex_session.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env bash
2+
3+
# "codex session strategy"
4+
#
5+
# OpenAI's `codex` CLI resumes a prior conversation with the `resume`
6+
# subcommand; `codex resume --last` reopens the most recent one. As with
7+
# Claude Code, re-launching the bare process starts a brand new conversation,
8+
# so restore rewrites the captured command to resume the last session.
9+
#
10+
# Behavior:
11+
# - If the saved command is already a `resume`/`fork` invocation, pass it
12+
# through unchanged (it targets a specific session the user picked).
13+
# - Otherwise restore the most recent session with `codex resume --last`.
14+
15+
ORIGINAL_COMMAND="$1"
16+
DIRECTORY="$2"
17+
18+
original_command_already_resumes() {
19+
[[ "$ORIGINAL_COMMAND" =~ (^|[[:space:]])(resume|fork)([[:space:]]|$) ]]
20+
}
21+
22+
main() {
23+
if original_command_already_resumes; then
24+
echo "$ORIGINAL_COMMAND"
25+
else
26+
echo "codex resume --last"
27+
fi
28+
}
29+
main

tests/test_agent_strategies.sh

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env bash
2+
# Agent CLI session restore: the claude/codex restore strategies rewrite a bare
3+
# launch into a resume, preserve an explicit resume, and are wired as defaults.
4+
5+
source "$(dirname "$0")/helpers/test_helpers.sh"
6+
7+
CLAUDE="$PLUGIN_DIR/strategies/claude_session.sh"
8+
CODEX="$PLUGIN_DIR/strategies/codex_session.sh"
9+
10+
assert_strategy() { # script desc input expected
11+
assert_eq "$("$1" "$3" "/some/dir")" "$4" "$2"
12+
}
13+
14+
# --- claude_session.sh ---
15+
assert_strategy "$CLAUDE" "bare claude gets --continue" \
16+
"claude" "claude --continue"
17+
assert_strategy "$CLAUDE" "claude flags preserved, --continue appended" \
18+
"claude --model opus" "claude --model opus --continue"
19+
assert_strategy "$CLAUDE" "claude --continue not duplicated" \
20+
"claude --continue" "claude --continue"
21+
assert_strategy "$CLAUDE" "claude --resume <id> preserved" \
22+
"claude --resume abc123" "claude --resume abc123"
23+
assert_strategy "$CLAUDE" "claude short -c preserved" \
24+
"claude -c" "claude -c"
25+
assert_strategy "$CLAUDE" "claude short -r <id> preserved" \
26+
"claude -r abc123" "claude -r abc123"
27+
28+
# --- codex_session.sh ---
29+
assert_strategy "$CODEX" "bare codex resumes last" \
30+
"codex" "codex resume --last"
31+
assert_strategy "$CODEX" "codex with prompt resumes last" \
32+
"codex fix the bug" "codex resume --last"
33+
assert_strategy "$CODEX" "codex --model flag still resumes last" \
34+
"codex --model gpt-5" "codex resume --last"
35+
assert_strategy "$CODEX" "codex resume returned as-is" \
36+
"codex resume" "codex resume"
37+
assert_strategy "$CODEX" "codex resume --last returned as-is" \
38+
"codex resume --last" "codex resume --last"
39+
assert_strategy "$CODEX" "codex resume <id> preserved" \
40+
"codex resume abc123" "codex resume abc123"
41+
assert_strategy "$CODEX" "codex fork <id> preserved" \
42+
"codex fork abc123" "codex fork abc123"
43+
44+
# --- wiring (default proc list + default strategy registration) ---
45+
source "$PLUGIN_DIR/scripts/variables.sh"
46+
assert_contains "$default_proc_list" "claude" "claude in default_proc_list"
47+
assert_contains "$default_proc_list" "codex" "codex in default_proc_list"
48+
49+
setup
50+
load_plugin
51+
strategies="$(tmuxp show-options -g 2>/dev/null)"
52+
teardown
53+
assert_contains "$strategies" "${restore_process_strategy_option}claude session" \
54+
"claude registered as 'session' strategy"
55+
assert_contains "$strategies" "${restore_process_strategy_option}codex session" \
56+
"codex registered as 'session' strategy"
57+
58+
# --- strategy files are executable ---
59+
[ -x "$CLAUDE" ] && _ok "claude_session.sh executable" || _ko "claude_session.sh executable"
60+
[ -x "$CODEX" ] && _ok "codex_session.sh executable" || _ko "codex_session.sh executable"
61+
62+
finish

0 commit comments

Comments
 (0)