The confession first

There are, at last count, a small army of tools that list your Claude Code sessions and let you jump back into one. tmux wrappers (claude-tmux, claunch), keyword resumers (tmux-claude-code), fleet managers (claude-manager), and a whole macOS menu-bar genre (claude-control, cmux, and friends). They’re good. Several are better-engineered than mine.

I built one more anyway.

Not because the others are wrong — because none of them were shaped like my day, and the cost of hand-rolling a 300-line script turned out to be smaller than the cost of bending my workflow around someone else’s defaults. That’s the whole pitch, and it’s a boring one. The interesting part is what I had to understand to build it, because it corrected a mental model I’d had backwards for months.

My day, concretely

I work off a single Linux box over SSH, from a few different machines. A session might be a homelab change, a side project, a blog post. I drop one mid-thought, my laptop sleeps, I pick it up that evening from a different terminal. The thing I kept doing was running claude --resume and squinting at a list of UUIDs trying to remember which 7f3a… was the one about the broken redirect.

I wanted one command — wt — that shows me every session with a human summary and tells me, truthfully, which ones are still alive. Then lets me pick one.

Simple ask. It sent me reading the on-disk format, and that’s where it got educational.

What I had backwards: tmux is not how you keep a session

Every tmux-first tool sells the same promise: run Claude inside tmux so your session survives a disconnect. I’d internalized that as “tmux is how Claude sessions persist.”

That’s wrong, and realizing it deleted half the code I thought I’d need.

A Claude Code session is one claude process, keyed by a sessionId UUID. Its entire transcript — every message, every tool call and result — is appended to a file:

~/.claude/projects/<cwd-slug>/<sessionId>.jsonl

It’s append-only, and it has no “end” marker. When you --resume, Claude reopens that same file and replays it. One of my session files spans three calendar days across half a dozen resumes — same file, same UUID, the whole conversation reconstructed from disk each time.

Which means: the history is durable independent of any running process. You do not need tmux to land exactly where you left off. claude --resume <id> does that from the transcript alone, on a box with no tmux installed at all.

So what is tmux for, then? Exactly one thing: keeping a process running while you’re disconnected — a long job, an agent grinding away, or re-attaching the same live process from your phone. That’s real, but it’s the exception, not the default. So in my tool, plain resume is the default and tmux is an opt-in flag. The inversion fell straight out of reading the format honestly.

The other thing the transcript doesn’t tell you: is it alive?

Here’s the subtle bit. The transcript tells you the history of a session. It does not tell you whether a claude process is running right now. There’s no “closed” record — the file for a long-dead session looks identical to one you left open thirty seconds ago.

Liveness lives somewhere else:

~/.claude/sessions/<pid>.json   →   { pid, sessionId, cwd, procStart, ... }

A session is alive if that pid is actually running. But you can’t just trust the file’s existence — it can linger after a crash — and you can’t just kill -0 the pid either, because the kernel recycles pids and you might be poking a process that reused the number. So the honest check is two-factor:

def alive(pid, procstart):
    try:
        os.kill(pid, 0)          # exists and signalable?
    except (ProcessLookupError, OSError):
        return False
    # ...and is it the SAME process, not a pid-recycle?
    stat = Path(f"/proc/{pid}/stat").read_text()
    starttime = stat[stat.rindex(")") + 2:].split()[19]
    return starttime == str(procstart)

That /proc/<pid>/stat start-time comparison is the difference between “I think it’s live” and “it’s live.” It’s the kind of detail you only get right by caring about the boring case.

With that, every session resolves to a real state:

  • â—Ź live — a process is running now
  • â§— waiting — no process; you left mid-conversation (last line was Claude)
  • · idle — no process; stale

And the payoff for getting liveness right: if you try to resume a session that’s still live in another terminal, the tool refuses to double-open it — two processes appending to one transcript is how you corrupt your own history — and offers a clean --fork-session instead.

The summaries were free the whole time

The feature I assumed I’d have to build — a short, human description of each session — I didn’t build at all. Claude Code already writes one. Buried in the transcript is a record type:

{"type": "ai-title", "aiTitle": "Investigate nested o directories", "sessionId": "..."}

Claude titles your sessions for you. The “summary” column in my tool is just that field, with a fallback to your last prompt. The best line of code is the one you delete after noticing the platform already did the work.

So what did I actually build

Not much, and that’s the point. wt is one Python file, standard library only, no daemon. It globs the transcripts, reads each one’s title and last-activity, joins that against the pid-verified live registry, sorts live-first, and prints a numbered list. Pick a number and it execs into claude --resume. There’s a -t for tmux when I genuinely need it, a d to archive old sessions (a file move, fully reversible), and a guarded hook that turns it into an SSH login greeting so the box tells me what’s on it the moment I land.

  watchtower · 5 session(s)
   1) â—Ź  live       16s  homelab    595e931d  Investigate nested o directories
   2) ·  idle     1d07h  notes-app  6565b121  Migrate to server components
  [#]resume  [t#]tmux  [d#]archive  [n]ew  [Enter]shell  [q]uit â–¸

If you want it, it’s on GitHub, MIT. But honestly, I’d rather you take the three things I had to learn than the tool:

  1. Your Claude history lives in a plain append-only JSONL on disk, not in tmux. --resume works without any wrapper. Back up ~/.claude/projects/ and you’ve backed up every conversation you’ve had.
  2. Liveness is a separate fact from history, and checking it honestly means verifying the pid is the same process — not just that something answers to the number.
  3. The platform probably already did the boring work (here: the titles). Read the format before you write the feature.

The flooded-market thing turns out not to matter. A tool that fits your own hands is worth building even when fifty others exist — especially when it’s small enough that “build” and “understand the system underneath” are the same afternoon.