# Per-tab Claude Code session resume in agterm (zsh): each tab resumes its own conversation after a terminal restart

> Source: <https://gist.github.com/ssgreg/874f4cd6ea943badb417dbd8b06f959b>
> Published: 2026-07-01 10:41:05+00:00

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.

- 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.

**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** 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.`claude`

command**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.

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.

```
# 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
}
```

**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`

.
