<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Developer-Tools on hippotion</title><link>https://blog.hippotion.com/tags/developer-tools/</link><description>Recent content in Developer-Tools on hippotion</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Fri, 30 Jan 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.hippotion.com/tags/developer-tools/index.xml" rel="self" type="application/rss+xml"/><item><title>🪟 I Built Yet Another Claude Code Session Switcher</title><link>https://blog.hippotion.com/posts/hand-rolled-claude-session-switcher/</link><pubDate>Fri, 30 Jan 2026 00:00:00 +0000</pubDate><guid>https://blog.hippotion.com/posts/hand-rolled-claude-session-switcher/</guid><description>The web is flooded with Claude Code session managers. I built one more anyway — and the part worth sharing isn&amp;rsquo;t the tool, it&amp;rsquo;s what I had to learn about where Claude actually keeps your sessions.</description><content:encoded><![CDATA[<h2 id="the-confession-first">The confession first</h2>
<p>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
(<a href="https://github.com/nielsgroen/claude-tmux">claude-tmux</a>,
<a href="https://github.com/0xkaz/claunch">claunch</a>), keyword resumers
(<a href="https://github.com/MaxGhenis/tmux-claude-code">tmux-claude-code</a>), fleet
managers (<a href="https://github.com/raphaelbgr/claude-manager">claude-manager</a>), and a
whole macOS menu-bar genre (<a href="https://github.com/sverrirsig/claude-control">claude-control</a>,
cmux, and friends). They&rsquo;re good. Several are better-engineered than mine.</p>
<p>I built one more anyway.</p>
<p>Not because the others are wrong — because none of them were shaped like <em>my</em>
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&rsquo;s defaults. That&rsquo;s the whole
pitch, and it&rsquo;s a boring one. The interesting part is what I had to understand to
build it, because it corrected a mental model I&rsquo;d had backwards for months.</p>
<h2 id="my-day-concretely">My day, concretely</h2>
<p>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 <code>claude --resume</code> and squinting at a list of UUIDs trying
to remember which <code>7f3a…</code> was the one about the broken redirect.</p>
<p>I wanted one command — <code>wt</code> — that shows me every session with a human summary
and tells me, truthfully, which ones are still alive. Then lets me pick one.</p>
<p>Simple ask. It sent me reading the on-disk format, and that&rsquo;s where it got
educational.</p>
<h2 id="what-i-had-backwards-tmux-is-not-how-you-keep-a-session">What I had backwards: tmux is not how you keep a session</h2>
<p>Every tmux-first tool sells the same promise: run Claude <em>inside</em> tmux so your
session survives a disconnect. I&rsquo;d internalized that as <strong>&ldquo;tmux is how Claude
sessions persist.&rdquo;</strong></p>
<p>That&rsquo;s wrong, and realizing it deleted half the code I thought I&rsquo;d need.</p>
<p>A Claude Code session is one <code>claude</code> process, keyed by a <code>sessionId</code> UUID. Its
entire transcript — every message, every tool call and result — is appended to a
file:</p>
<pre tabindex="0"><code>~/.claude/projects/&lt;cwd-slug&gt;/&lt;sessionId&gt;.jsonl
</code></pre><p>It&rsquo;s append-only, and it has <strong>no &ldquo;end&rdquo; marker</strong>. When you <code>--resume</code>, 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.</p>
<p>Which means: <strong>the history is durable independent of any running process.</strong> You
do not need tmux to land exactly where you left off. <code>claude --resume &lt;id&gt;</code> does
that from the transcript alone, on a box with no tmux installed at all.</p>
<p>So what <em>is</em> tmux for, then? Exactly one thing: keeping a process <em>running</em> while
you&rsquo;re disconnected — a long job, an agent grinding away, or re-attaching the
<em>same live process</em> from your phone. That&rsquo;s real, but it&rsquo;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.</p>
<h2 id="the-other-thing-the-transcript-doesnt-tell-you-is-it-alive">The other thing the transcript doesn&rsquo;t tell you: is it alive?</h2>
<p>Here&rsquo;s the subtle bit. The transcript tells you the <em>history</em> of a session. It
does <strong>not</strong> tell you whether a <code>claude</code> process is running right now. There&rsquo;s no
&ldquo;closed&rdquo; record — the file for a long-dead session looks identical to one you
left open thirty seconds ago.</p>
<p>Liveness lives somewhere else:</p>
<pre tabindex="0"><code>~/.claude/sessions/&lt;pid&gt;.json   →   { pid, sessionId, cwd, procStart, ... }
</code></pre><p>A session is alive if that pid is actually running. But you can&rsquo;t just trust the
file&rsquo;s existence — it can linger after a crash — and you can&rsquo;t just <code>kill -0</code> the
pid either, because the kernel recycles pids and you might be poking a process
that <em>reused</em> the number. So the honest check is two-factor:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">alive</span><span class="p">(</span><span class="n">pid</span><span class="p">,</span> <span class="n">procstart</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">os</span><span class="o">.</span><span class="n">kill</span><span class="p">(</span><span class="n">pid</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>          <span class="c1"># exists and signalable?</span>
</span></span><span class="line"><span class="cl">    <span class="k">except</span> <span class="p">(</span><span class="ne">ProcessLookupError</span><span class="p">,</span> <span class="ne">OSError</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...and is it the SAME process, not a pid-recycle?</span>
</span></span><span class="line"><span class="cl">    <span class="n">stat</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;/proc/</span><span class="si">{</span><span class="n">pid</span><span class="si">}</span><span class="s2">/stat&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">read_text</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">starttime</span> <span class="o">=</span> <span class="n">stat</span><span class="p">[</span><span class="n">stat</span><span class="o">.</span><span class="n">rindex</span><span class="p">(</span><span class="s2">&#34;)&#34;</span><span class="p">)</span> <span class="o">+</span> <span class="mi">2</span><span class="p">:]</span><span class="o">.</span><span class="n">split</span><span class="p">()[</span><span class="mi">19</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">starttime</span> <span class="o">==</span> <span class="nb">str</span><span class="p">(</span><span class="n">procstart</span><span class="p">)</span>
</span></span></code></pre></div><p>That <code>/proc/&lt;pid&gt;/stat</code> start-time comparison is the difference between &ldquo;I think
it&rsquo;s live&rdquo; and &ldquo;it&rsquo;s live.&rdquo; It&rsquo;s the kind of detail you only get right by caring
about the boring case.</p>
<p>With that, every session resolves to a real state:</p>
<ul>
<li><code>● live</code> — a process is running now</li>
<li><code>⧗ waiting</code> — no process; you left mid-conversation (last line was Claude)</li>
<li><code>· idle</code> — no process; stale</li>
</ul>
<p>And the payoff for getting <em>liveness</em> right: if you try to resume a session that&rsquo;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 <code>--fork-session</code> instead.</p>
<h2 id="the-summaries-were-free-the-whole-time">The summaries were free the whole time</h2>
<p>The feature I assumed I&rsquo;d have to build — a short, human description of each
session — I didn&rsquo;t build at all. Claude Code already writes one. Buried in the
transcript is a record type:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span><span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;ai-title&#34;</span><span class="p">,</span> <span class="nt">&#34;aiTitle&#34;</span><span class="p">:</span> <span class="s2">&#34;Investigate nested o directories&#34;</span><span class="p">,</span> <span class="nt">&#34;sessionId&#34;</span><span class="p">:</span> <span class="s2">&#34;...&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>Claude titles your sessions for you. The &ldquo;summary&rdquo; 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.</p>
<h2 id="so-what-did-i-actually-build">So what did I actually build</h2>
<p>Not much, and that&rsquo;s the point. <code>wt</code> is one Python file, standard library only,
no daemon. It globs the transcripts, reads each one&rsquo;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 <code>exec</code>s into <code>claude --resume</code>. There&rsquo;s a
<code>-t</code> for tmux when I genuinely need it, a <code>d</code> 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&rsquo;s on it the moment I land.</p>
<pre tabindex="0"><code>  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 ▸
</code></pre><p>If you want it, it&rsquo;s <a href="https://github.com/janos-gyorgy/claude-watchtower">on GitHub</a>,
MIT. But honestly, I&rsquo;d rather you take the three things I had to learn than the
tool:</p>
<ol>
<li><strong>Your Claude history lives in a plain append-only JSONL on disk, not in
tmux.</strong> <code>--resume</code> works without any wrapper. Back up <code>~/.claude/projects/</code> and
you&rsquo;ve backed up every conversation you&rsquo;ve had.</li>
<li><strong>Liveness is a separate fact from history</strong>, and checking it honestly means
verifying the pid <em>is the same process</em> — not just that something answers to
the number.</li>
<li><strong>The platform probably already did the boring work</strong> (here: the titles). Read
the format before you write the feature.</li>
</ol>
<p>The flooded-market thing turns out not to matter. A tool that fits your own hands
is worth building even when fifty others exist — <em>especially</em> when it&rsquo;s small
enough that &ldquo;build&rdquo; and &ldquo;understand the system underneath&rdquo; are the same afternoon.</p>
]]></content:encoded></item></channel></rss>