I had four Claude Code sessions open in iTerm tabs, all of them mid-task, and then I needed to leave the desk. Every one was reachable from exactly one place: that laptop, that window, that chair.
So I built claude2tmux
One command reads the tabs of the iTerm2 window you ran it from, finds the Claude sessions inside them, and moves each one into tmux. Same working directory, same conversation. After that you can detach, shut the laptop, and pick all of them back up over SSH from another machine.
Running it bare changes nothing. It prints a table and stops. --go is the flag that actually performs the move.
$ claude2tmux iTerm2 window 1, 5 tab(s) -> tmux session 'claude': (1 other iTerm2 window not touched) MOVE tab 1 ttys000 pid 39902 -> claude:api ~/code/api add retry backoff to the webhook sender STAGE tab 2 ttys001 pid 36339 -> claude:dotfiles ~/code/dotfiles this session -- tab 3 ttys014 zsh no Claude session MOVE tab 4 ttys003 pid 30259 -> claude:web ~/code/web login page flashes light before dark loads -- 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.
Every tab gets a line, including the ones where the answer is nothing. That is deliberate: a tool that skips something and says so is fine, and a tool that drops something and prints nothing teaches you to trust a listing that is lying by omission.
MOVE gets killed and reopened in tmux. The tab itself stays open — only the Claude process inside it is stopped, so the tab drops back to a shell prompt with its scrollback intact. -- is a tab with nothing to move: a shell, an editor, or tmux.
STAGE is the session you are typing in right now. It cannot kill itself, because that would stop the migration halfway through. So it opens the tmux window and types the resume command there without pressing Enter. You quit that tab yourself, switch over, and press it. Anything whose session id cannot be proven is staged the same way, with a bare claude --resume that opens Claude Code's own picker.
Why it kills your session
Because it has to. On Linux you would reach for reptyr, which attaches to a running process, opens a new terminal, and swaps the file descriptors underneath the program without it noticing. macOS has no working equivalent: a process keeps the controlling terminal it was born with, for as long as it lives.
So the honest description of this tool is not "move my sessions into tmux". It is "kill my sessions and convincingly pretend I didn't."
The pretending works because Claude Code already writes everything down. Every message is appended to a transcript file as it happens, not at exit — one file per session, named by session id, under ~/.claude/projects/. That changes what the running process is worth. It is not the session. It is the current reader of a file that already exists on disk. Kill the reader, start a new one pointed at the same file, and the conversation comes back.
# which directory the session is in — macOS makes you ask lsof cwd=$(lsof -a -p "$pid" -d cwd -Fn | sed -n 's/^n//p' | head -1) # kill the reader, start a new one on the same file kill -TERM "$pid" tmux new-window -d -t "=claude:" -n "$name" -c "$cwd" tmux send-keys -t "=claude:$name" "claude --resume $sid" C-m
Everything already written survives. A tool call that was in flight at the moment of the kill does not. That is the trade, and it is why the default invocation only prints a table. If it is not an acceptable trade for one particular session, --no-kill builds that session's tmux window and stages the resume command without touching the original.
How it knows which session is which
This is the part that has to be exactly right, because the failure mode is not an error message — it is resuming the wrong conversation, or killing a session that was never yours to touch. So nothing here is inferred. The chain is four hops, and every hop is a fact somebody else already wrote down.
tab → tty → the claude on that tty → its registry record → its id.
It starts by asking iTerm2, over AppleScript, for the tabs of the current window. Each one hands back its tty, which is the honest link between a tab you can see and a process you can signal.
tell application "iTerm2" repeat with t in tabs of current window repeat with s in sessions of t log (tty of s) -- /dev/ttys000 end repeat end repeat end tell
The session for a tab is then the claude process whose controlling terminal is exactly that tty, ignoring any process that has another claude above it in the parent chain — those are subagents running inside a session, not sessions of their own.
Scoping to real tabs is doing more work than it looks like. Two whole categories of mistake stop being possible, rather than being filtered out after the fact. 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 SIGTERMed out from under an app you were not even looking at. And a tab running tmux holds its Claude sessions on pane ttys rather than the tab's own tty, so anything already in tmux is invisible to the scan.
The last hop is the session id, and it is not something to work out from the outside — every running session already writes its own facts down, keyed by process id.
{ "pid": 39902,
"sessionId": "9f3c1e2a-4b7d-4c2e-9a11-6f0e5d3c8b42",
"cwd": "/Users/me/code/api",
"entrypoint": "cli",
"kind": "interactive",
"procStart": "Mon Aug 17 21:00:23 2026" }
sessionId is the process's own id, which matters more than it sounds: several sessions can share one working directory, so anything that reasons from directories will happily hand you your neighbour's conversation. entrypoint is cli, claude-desktop or claude-vscode, which is what the --system mode below uses to stay out of the app's business.
procStart is the subtle one. A registry file outlives a process that was hard-killed, and process ids get reused, so a stale record can describe a pid that now belongs to something else entirely. Comparing that timestamp against the process's real start time is what stops a long-dead session from being resurrected under a new pid. It is recorded in UTC while ps -o lstart= prints local time, which is a six-hour discrepancy on my machine and looks exactly like a bug until you convert one of them.
Two guarantees ride on top, both the same instinct as the table in FIG 1. A session is only offered for --resume if its transcript actually exists on disk, so nothing gets killed that cannot be brought back. And two live processes claiming one id are reported rather than quietly opened as two windows onto one conversation.
When there is no iTerm2 to ask
Run it from Terminal.app, Ghostty, an SSH session or a cron job and there are no tabs to enumerate. --system is that mode, and it is also what happens automatically when iTerm2 is not running: walk every Claude process on the machine instead, keeping the ones whose registry record says entrypoint: cli, that have a controlling terminal, and that are not already sitting on a tty tmux is driving.
What it will not do is quietly switch to that mode. If iTerm2 is running but reports nothing readable — one window whose session has ended is enough to make the AppleScript throw — the run stops and says so rather than falling back, because silently widening what a tool operates on is the same class of bug as killing the wrong session.
Get it
# Homebrew brew tap daksh-gargas/tap brew install claude2tmux # or take the one file straight from the repo curl -fsSL https://raw.githubusercontent.com/daksh-gargas/claude2tmux/main/claude2tmux \ -o /usr/local/bin/claude2tmux && chmod +x /usr/local/bin/claude2tmux
It is 721 lines of bash at github.com/daksh-gargas/claude2tmux, and it exists because a process cannot be handed a new terminal, but a transcript can be handed to a new process.