Per-tab Claude Code session resume in agterm #71
Replies: 4 comments 1 reply
Per-tab Codex CLI session resume in agtermSame idea as Claude Code: each tab reopens its own codex conversation after restart instead of the shared "most recent" one. Key difference: codex won't accept a session id at launch, so instead of tab-id == session-id, the function maps tab-id → codex-session-id in The functioncodex() {
emulate -L zsh
setopt local_options null_glob
local sid=${AGTERM_SESSION_ID:l}
[[ -z $sid ]] && { command codex "$@"; return; }
local a
for a in "$@"; do
case $a in
-h|--help|-V|--version|--remote|--remote=*|\
exec|e|review|login|logout|mcp|plugin|mcp-server|app-server|remote-control|app|completion|\
update|doctor|sandbox|debug|apply|a|resume|archive|delete|unarchive|fork|cloud|exec-server|features|help)
command codex "$@"; return ;;
esac
done
local mapf=~/.codex/agterm/$sid
local cid; [[ -f $mapf ]] && cid=$(<$mapf)
local -a s=(~/.codex/sessions/**/rollout-*-$cid.jsonl(N))
if [[ -n $cid && $#s ]]; then
command codex resume "$cid" "$@"; return
fi
local before=$(mktemp)
command codex "$@"; local rc=$?
local -a new=(~/.codex/sessions/**/rollout-*.jsonl(N.om))
if (( $#new )) && [[ $new[1] -nt $before ]]; then
local uuid=${${new[1]:t:r}[-36,-1]}
[[ $uuid == ????????-????-????-????-???????????? ]] && { mkdir -p ${mapf:h}; print -r -- $uuid > $mapf; }
fi
command rm -f $before
return $rc
}
Where: ~/.zshrc
Activate: source ~/.zshrc
Verify: start codex, run a command, restart agterm—the tab resumes that session. |
|
both functions from this thread are now in the repo under cookbook/, a collection of installable the READMEs reuse your own wording rather than a rewrite. The Claude one keeps all seven limitations as written, including that it rests on restore behavior verified by its author rather than documented, and that a split or scratch pane shares one two things I noticed reading the codex function, neither changed in your code, both recorded under Limits as my observations: the mapping is written only after if either of you would rather your function not be in the repo, or want the wording changed, say so and I will take it out or fix it. |
Per-tab opencode session resume in agtermSame idea as the two above: each tab reopens its own opencode conversation after a restart instead of starting a fresh one. Key difference from @brusnigin's codex function: opencode won't accept a session id at launch either, so this one also maps tab-id → session-id, in Two things about opencode shaped the rest. Checked against opencode 1.18.10. The functions# opencode: per-agterm-tab session resume.
# Maps the tab's own uuid (AGTERM_SESSION_ID) to an opencode session id under
# ~/.local/state/opencode/agterm/, so restoring the terminal resumes that tab's own
# conversation instead of starting fresh.
# Requires: agterm (exports AGTERM_SESSION_ID, restores running commands) + zsh.
# Session ids of the current project, most recently updated first.
# opencode keeps sessions in sqlite, so the CLI is the only supported way to ask. The list is
# scoped to the project while --session is not scoped at all, so anything listed is resumable.
_agterm_opencode_sessions() {
emulate -L zsh
local line
command opencode session list --format json 2>/dev/null | while read -r line; do
[[ $line == '"id": "ses_'* ]] || continue # one field per line, so a title cannot fake an id line
line=${line#*: \"}
print -r -- ${line%%\"*}
done
}
opencode() {
emulate -L zsh
local sid=${AGTERM_SESSION_ID:l}
[[ -z $sid ]] && { command opencode "$@"; return; } # not in an agterm tab -> passthrough
local mapf=${XDG_STATE_HOME:-$HOME/.local/state}/opencode/agterm/$sid
local cid; [[ -f $mapf ]] && cid=$(<$mapf)
if [[ $1 == (-s|--session) && -n $cid && $2 == $cid ]]; then
shift 2 # restore replayed our own flag -> re-decide below
else
local a
for a in "$@"; do # user steering a session/subcommand?
case $a in
-s|--session|--session=*|-c|--continue|--fork|-h|--help|-v|--version|\
run|attach|serve|web|acp|mcp|models|stats|export|import|session|plugin|plug|db|\
debug|agent|providers|auth|github|pr|upgrade|uninstall|completion)
command opencode "$@"; return ;; # -> hand off untouched
esac
done
fi
local -a before=( ${(f)"$(_agterm_opencode_sessions)"} ) # what existed before this run
if [[ -n $cid ]] && (( ${before[(Ie)$cid]} )); then
command opencode --session $cid "$@" # this tab's session is still there -> continue it
return
fi
command opencode "$@" # nothing to resume -> plain run, then adopt what it made
local rc=$? id
for id in ${(f)"$(_agterm_opencode_sessions)"}; do
[[ -n $id ]] || continue
(( ${before[(Ie)$id]} )) && continue # was already there -> not this run's
command mkdir -p ${mapf:h} && print -r -- $id > $mapf
break
done
return $rc
}Where: Limits worth knowing
I have this written up as a cookbook recipe as well — Edited 31 Jul: corrected the claim that |
Uh oh!
There was an error while loading. Please reload this page.
Per-tab Claude Code session resume in agterm
Keep several Claude Code sessions open at once? With this, after the terminal
restarts each tab resumes its own conversation instead of a shared
"most recent" one.
What it's good at
restart, each reopening exactly its own conversation.
AGTERM_SESSION_ID).No mapping file to keep in sync or let go stale.
(
--session-id); later launches continue it (--resume) - it flipsautomatically.
claude mcp,-p, an explicitclaude --resume <other-id>, and any launch outside agterm pass throughuntouched.
emulate -L),so nothing leaks into your interactive shell.
Limitations
feeding the restored command back through the login shell (that is how the
function intercepts it). It won't work as-is in another terminal - you'd adapt
it to that terminal's equivalent.
it could change between agterm versions.
claudeprocesses in the same tab (a split/scratch pane shares oneAGTERM_SESSION_ID) collide with "Session ID ... is already in use".claudecommand with a shell function. Passthrough ishandled, but the list of subcommands/flags that must not be touched has to be
kept current if the CLI grows new ones.
~/.claude/projects/*/<id>.jsonl).If that storage location changes, the "does this conversation exist" check
breaks (it would always create, then hit "already in use" on restart). A
one-line fix, but worth knowing.
${:l}, the(N)glob qualifier,emulate). Bash needs arewrite.
first launch in a tab starts a new conversation pinned to that tab - it does
not adopt an arbitrary earlier one.
How it works (plain)
agterm can remember the command running in a tab and re-run it on restart. The
catch: it re-runs the command verbatim, and
claudewith no arguments is anew conversation, not a continuation.
The trick rests on two facts. Every agterm tab has a stable identifier
(
AGTERM_SESSION_ID) that survives a restart. And Claude Code lets you supply asession id from the outside (
--session-id). So you can use the tab's id as theconversation id - and the tab permanently "owns" one conversation.
The function then wraps
claude: on the first launch in a tab it pins theconversation to the tab id (
--session-id); if that conversation's file alreadyexists on disk it continues it (
--resume). The whole decision is one check: is<tab-id>.jsonlpresent under~/.claude/projects/?On restart agterm replays the remembered command, the function recognizes "its
own" id, sees the conversation already exists, and reopens exactly that one.
Each tab, its own.
The function
Where and how
~/.zshrc- the config your interactive login zsh reads. Thatmatters: agterm feeds the restored command into that shell, so the function
must be defined there (not in
.zshenv/.zprofile).enabled (Settings); Claude Code sessions stored in the default
~/.claude/projects/.shell run
source ~/.zshrc.claude, say a few words, restart the terminal -the tab should return to the same conversation.
pswill showclaude --resume <tab-id>.~/.zshrc.All reactions