macOS · zsh · tmux
You start three Claude Code sessions in iTerm, get deep into all of them, and then realise none are reachable from anywhere but that one window. claude2tmux moves them into tmux — detach, close the laptop, pick them back up over SSH.
$ claude2tmux iTerm2 window 1, 5 tab(s) -> tmux session 'claude': (1 other iTerm2 window(s) not touched) MOVE tab 1 ttys000 pid 39902 -> claude:D4G4 ~/GitHub/D4G4 People don't realise how crappy their sound is… STAGE tab 2 ttys001 pid 36339 -> claude:dg ~ this session -- tab 3 ttys014 zsh no Claude session MOVE tab 4 ttys003 pid 30259 -> claude:First-Ink ~/GitHub/D4G4/First-Ink lets make a new build -- tab 5 ttys015 tmux running tmux — its sessions are already in tmux MOVE (2) SIGTERMed, then relaunched as claude --resume <id>. The iTerm2 tab stays open — only the Claude process is stopped. STAGE (1) window built, command typed but not entered — you quit the original and press Enter. Never killed for you. Listing only. Run --go to perform it.
It does not move the running process. A live process's controlling
terminal cannot be reassigned on macOS — the Linux tool for that
(reptyr) works by ptrace-ing the target and
swapping its TTY, and there is no working macOS equivalent.
So claude2tmux migrates the session, not the process. Claude Code
appends every message to
~/.claude/projects/<slug>/<id>.jsonl as it
happens, so killing a session and running
claude --resume <id> inside tmux restores the full
conversation, working directory, and history.
What survives: everything already written to the
transcript.
What is lost: a tool call that was in flight at the
moment of the kill.
If that trade isn't acceptable for a given session, --no-kill
builds its tmux window and stages the resume command without touching the
original — you quit it yourself once it's idle.
The full user/tap/formula name is deliberate. Since Homebrew
6.0.0 a formula from a non-official tap has to be trusted before Homebrew
will load it — a formula is executable Ruby, not metadata — and naming it
in full is that grant, for this one formula. The two-step
brew tap + brew install claude2tmux form stops
with a brew trust error instead.
One file, no dependencies beyond tmux and python3.
Or grab the
latest release tarball
and install -m 755 claude2tmux /usr/local/bin/.
c2t shortcut
Writes a marked block to ~/.zshrc, so re-running updates it in
place instead of appending a second copy. It backs the file up first, and
refuses to install if you already define c2t elsewhere.
--uninstall-c2t restores the file byte-for-byte.
The listing states a disposition per session, so what actually moves is never something you have to infer.
| Disposition | Meaning |
|---|---|
| MOVE | SIGTERMed, then relaunched as claude --resume <id> in a tmux window at the same cwd. |
| STAGE | Window built, resume command typed but not entered. Never killed for you. |
| -- | A tab with no Claude session in it — a shell, tmux, an editor. Listed so you can see it was checked. |
| SKIP | A session that exists but is not safely movable. Every reason is named. |
It walks the tabs of the iTerm2 window you ran it from, so two whole classes of mistake are impossible by construction rather than by filtering. A Claude desktop app or VS Code session runs on a pty that is not an iTerm2 tab, so it can never be picked up. And a tab running tmux holds its Claude sessions on pane ttys rather than the tab's own tty, so sessions already in tmux are never touched.
The iTerm2 tab is left open — only the Claude process inside it is stopped, so the tab drops back to its shell prompt with its scrollback intact.
The session you run the command from is always STAGE, never MOVE — killing it would abort the migration mid-run. Quit that tab yourself, switch to its window, press Enter.
Bare claude2tmux is identical to --list, and
--list overrides --go — asking to see the plan
never executes it. An unrecognised flag exits 2 rather than being ignored,
since a typo next to --go would otherwise be a bad surprise.
Every hop is a fact rather than a guess:
tab → tty → the claude on that tty →
~/.claude/sessions/<pid>.json → id. iTerm2 is asked for its
tabs and hands back each tab's tty; the session's own id is read from the
registry record it writes for itself, never inferred from the working
directory — one directory can hold several sessions at once.
The full write-up, listings included, is in
You can't move a terminal, so move
the session.