Skip to content

Instantly share code, notes, and snippets.

@ssgreg
Created July 1, 2026 10:41
Show Gist options
  • Select an option

  • Save ssgreg/874f4cd6ea943badb417dbd8b06f959b to your computer and use it in GitHub Desktop.

Select an option

Save ssgreg/874f4cd6ea943badb417dbd8b06f959b to your computer and use it in GitHub Desktop.
Per-tab Claude Code session resume in agterm (zsh): each tab resumes its own conversation after a terminal restart

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

  • Solves a concrete pain: multiple parallel Claude Code sessions survive a restart, each reopening exactly its own conversation.
  • Zero state: the conversation id is the tab's uuid (AGTERM_SESSION_ID). No mapping file to keep in sync or let go stale.
  • Idempotent: the first launch pins the conversation to the tab (--session-id); later launches continue it (--resume) - it flips automatically.
  • Stays out of the way: claude mcp, -p, an explicit claude --resume <other-id>, and any launch outside agterm pass through untouched.
  • Small, dependency-free, pure zsh; options are function-local (emulate -L), so nothing leaks into your interactive shell.

Limitations

  • agterm-specific. It needs a stable per-tab identifier and relies on agterm 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.
  • Rests on restore behavior that is verified empirically, not documented - it could change between agterm versions.
  • One conversation per tab. Since conversation id = tab id, two live claude processes in the same tab (a split/scratch pane shares one AGTERM_SESSION_ID) collide with "Session ID ... is already in use".
  • It shadows the claude command with a shell function. Passthrough is handled, but the list of subcommands/flags that must not be touched has to be kept current if the CLI grows new ones.
  • It knows Claude Code's on-disk layout (~/.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.
  • zsh only (${:l}, the (N) glob qualifier, emulate). Bash needs a rewrite.
  • Existing conversations aren't bound to tabs. After you install this, the 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 claude with no arguments is a new 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 a session id from the outside (--session-id). So you can use the tab's id as the conversation id - and the tab permanently "owns" one conversation.

The function then wraps claude: on the first launch in a tab it pins the conversation to the tab id (--session-id); if that conversation's file already exists on disk it continues it (--resume). The whole decision is one check: is <tab-id>.jsonl present 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

# Claude Code: per-agterm-tab session resume.
# Binds each tab's Claude Code session id to the tab's own uuid (AGTERM_SESSION_ID),
# so restoring the terminal resumes that tab's own conversation instead of starting fresh.
# Requires: agterm (exports AGTERM_SESSION_ID, restores running commands) + zsh.
claude() {
  emulate -L zsh
  local sid=${AGTERM_SESSION_ID:l}
  [[ -z $sid ]] && { command claude "$@"; return; }             # not in an agterm tab -> passthrough

  if [[ "$1" == (--session-id|-r|--resume) && "${2:l}" == $sid ]]; then
    shift 2                                                      # restore replayed our own flag -> re-decide below
  else
    local a
    for a in "$@"; do                                           # user steering a session/subcommand/headless?
      case $a in
        -r|--resume|--session-id|--resume=*|--session-id=*|-r=*|-c|--continue|-p|--print|-v|--version|-h|--help|mcp|update|doctor|config|install|migrate-installer|setup-token)
          command claude "$@"; return ;;                        # -> hand off untouched
      esac
    done
  fi

  local -a f=(~/.claude/projects/*/$sid.jsonl(N))               # does this tab's session already exist?
  if (( $#f )); then command claude --resume "$sid" "$@"        # yes -> continue it
  else command claude --session-id "$sid" "$@"; fi              # no  -> create it with this fixed id
}

Where and how

  • File: ~/.zshrc - the config your interactive login zsh reads. That matters: agterm feeds the restored command into that shell, so the function must be defined there (not in .zshenv / .zprofile).
  • Requirements: zsh; agterm with "Restore running commands on restart" enabled (Settings); Claude Code sessions stored in the default ~/.claude/projects/.
  • Activate: new tabs/shells pick it up automatically; in an already-open shell run source ~/.zshrc.
  • Verify: open a tab, run claude, say a few words, restart the terminal - the tab should return to the same conversation. ps will show claude --resume <tab-id>.
  • Remove: delete the function block from ~/.zshrc.
# Claude Code: per-agterm-tab session resume.
# Binds each tab's Claude Code session id to the tab's own uuid (AGTERM_SESSION_ID),
# so restoring the terminal resumes that tab's own conversation instead of starting fresh.
# Requires: agterm (exports AGTERM_SESSION_ID, restores running commands) + zsh.
# Add to ~/.zshrc.
claude() {
emulate -L zsh
local sid=${AGTERM_SESSION_ID:l}
[[ -z $sid ]] && { command claude "$@"; return; } # not in an agterm tab -> passthrough
if [[ "$1" == (--session-id|-r|--resume) && "${2:l}" == $sid ]]; then
shift 2 # restore replayed our own flag -> re-decide below
else
local a
for a in "$@"; do # user steering a session/subcommand/headless?
case $a in
-r|--resume|--session-id|--resume=*|--session-id=*|-r=*|-c|--continue|-p|--print|-v|--version|-h|--help|mcp|update|doctor|config|install|migrate-installer|setup-token)
command claude "$@"; return ;; # -> hand off untouched
esac
done
fi
local -a f=(~/.claude/projects/*/$sid.jsonl(N)) # does this tab's session already exist?
if (( $#f )); then command claude --resume "$sid" "$@" # yes -> continue it
else command claude --session-id "$sid" "$@"; fi # no -> create it with this fixed id
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment