Raising Claude From the Dead With a Bash Script

A while ago I wrote Claude Chats. Put simply, it logs every message going into and out of Claude Code to a pgvector database, with embeddings for semantic search, and hands the lot back to Claude as an MCP. I have dozens of projects and a terrible memory, so it's one of the most useful things I've ever written.
It has one gap, though. It remembers what was said. It doesn't get me back into the room.
A dozen windows and a sieve
At any given moment I have about a dozen Claude Code windows open. When I counted on Friday it was eleven: tinycrypt, a DDR timing hunt, a pewter casting project, a TV app, and a handful whose names I couldn't have told you without looking. When a project's done, I close its window.
Then, a week later, I want to ask that conversation a follow-up question. Sometimes I want to ask it myself. More and more, I want the Claude I'm currently talking to to ask it, because Claude Code has a SendMessage tool (and ListAgents to go with it) that lets one live session send a message to another on the same machine. My current Claude gets the answer without having to load a three-day transcript into its own context.
The catch is in the word live. SendMessage can only reach sessions that are running, and the session I want is usually one I closed.
How it used to go
On Thursday afternoon a session called aesop was about to re-embed 29,499 rows of conversation history. Before it started, it noticed something. That morning, a different session, VictoriaMetrics-locally, had built a nightly archive of old months to S3, and the archive decided whether a month had changed by its row count and the total length of its text. A re-embed changes neither. So the database would get the new vectors and the backups would quietly keep the old ones, and the first restore would bring the bug straight back.
The right thing was to ask the session that built the archive. It wasn't running.
So I found it. Claude Chats told me its name and folder, and I cd'd there and typed claude --resume, like an animal. Then:
Ok, I've raised
VictoriaMetrics-locallyfrom the dead. Please discuss it with him
aesop sent its question across. The message sat there for a moment, held, because the two sessions were in different permission modes (an incoming message is held until you approve it in the other window, which I hadn't known). Once I'd approved it, the reply came back inside a minute:
Re-embed vs HOME-458 archive: you're right, and it's a real hole — please keep holding; I'll fix the archiver first.
It then explained why the obvious fix, hashing the vectors themselves, would mean reading about 240 MB per month every night. It proposed a cheap fingerprint of which embedder had produced each month's rows instead. One Claude caught the bug and the other designed the fix, and I mostly pressed Enter.
It was brilliant. The finding-and-resurrecting part was not.

Six minutes on a Friday
The next day I went looking for the conversation where I'd designed a pewter hook for my tripod. It took two sessions and some detective work to find out it was called upside-down-cats (it had started as something else entirely). Then I asked:
Would it be possible to write a script (~/scripts) that would take a string as a parameter to a Claude (haiku?) to find the conversation and then check if that conversation is already open. If it's open, the Terminal window should be restored if it's minimized, then brought to the front (the original terminal window should exit). If not open, the terminal should
cdto the right directory and launchclaude --resume. A shell script would be preferable, but Python as a second choice, or Rust third
Rust was third because a Rust binary would have to go into my Mac Ansible playbook, with all the ceremony that brings. A bash script just goes in ~/scripts.
I asked at 13:37. At 13:43 I said "Perfect, thanks". The result is cjump, and you can find the source in the claude-chats repo.
cjump "building a pewter hook for my tripod"
cjump 76843580-8293-4607-b5f5-15b8e11b980c # a session ID skips the search
cjump -n "…" # dry run: say what it would do
cjump -k "…" # keep this window open afterwards
How it works
flowchart TD
A["cjump 'a pewter hook for my tripod'"] --> B["search Claude Chats (pgvector on Luke)"]
B --> C[one Haiku call picks the session]
C --> D{"is it running?<br/>~/.claude/sessions/*.json"}
D -- yes --> E[find its tty, raise that Terminal tab]
E --> F[close the window cjump ran in]
D -- no --> G[read its folder from the transcript]
G --> H["cd there, exec claude --resume"]
It does three things.
1. Find the session. The script calls Claude Chats' own search_memory function through uv run, using the same code and environment as the MCP. It groups the hits by session and hands the candidates to Haiku in a single call, with a JSON schema for the answer:
claude -p --model haiku --no-session-persistence \
--strict-mcp-config --setting-sources project --tools "" \
--output-format json --json-schema "$schema" -- "$prompt"
The prompt asks Haiku to pick "the session where the work itself happened" and to prefer it over later catch-up sessions that merely discuss it. That isn't hypothetical: there's a perfectly good session called Pewter-tripod-hook conversation catch-up that contains no pewter whatsoever. If Haiku's confidence comes back low, the script asks before going any further.
2. Is it already open? Claude Code keeps a small JSON file in ~/.claude/sessions/ for every running session, named after its process ID and holding its session ID. Find the matching file, check the process is still alive, and ps tells you which tty it's on. Then a bit of AppleScript goes through Terminal's windows and tabs looking for that tty. It un-minimises the window, selects the tab and brings it to the front.
Then it closes the window cjump was run from, so I don't end up with thirteen windows. It waits until that tab has gone back to an idle shell first, otherwise Terminal asks whether you really want to terminate the running processes. (You do. The running process is the script doing the closing.)
3. If it isn't open, resurrect it. The transcript's first cwd is the folder the session belongs to, which is where claude --resume will look for it. So the script cds there and execs Claude in place of itself.
That's it. About 190 lines of bash, most of them AppleScript and comments.
"20s? Ouch, would Sonnet be faster?"
The first version took twenty seconds.
It let Haiku drive the conversation-memory MCP itself. It worked and found the right session for about a penny, and then I sat and watched the cursor for nineteen and a half seconds. My instinct was to throw a bigger model at it. Claude's instinct, to its credit, was to measure first, and Sonnet would have been slower. Haiku wasn't the bottleneck:
- about 5 s was
claude -pstarting up, hooks and MCP servers included - the rest was Haiku making tool calls one after another: find the tool, call the search, have a look at the results, think, answer
So the script now does the search itself, which takes 2.9 s against Luke's database. Haiku gets one call, with no tools to call and nothing to look up. Stripping out the MCP servers and my user settings took a bare claude -p from 5.3 s to 2.6 s on its own. The whole thing now takes five to six seconds, end to end, window raised.
The lesson I keep re-learning: when an LLM call is slow, the model is rarely the slow part. The slow part is everything you've wrapped around it.

Things that bit
Three small bugs, each of them the kind that only happens in bash or AppleScript.
The window that came third. The first real test raised the right window to position three. AppleScript loop variables like w in repeat with w in windows are positional references, the third window, not this window. As soon as you start reordering windows, they point at whatever happens to be third now. The fix is to find the window's id first and act on window id wid from then on.
The apostrophe. My first real run from a Terminal tab died with:
/Users/nakomis/scripts/cjump: line 193: unexpected EOF while looking for matching `"'
The cause was the word window's in an AppleScript comment, inside a heredoc, inside $(...). macOS still ships bash 3.2, and bash 3.2 tries to parse quotes inside a heredoc that sits within command substitution. One apostrophe in a comment unbalanced the entire script. The fix was rewording the comment, which is the least satisfying fix there is.
The silent exit. A day later, cjump found a session, printed Found:… and then simply stopped. No error, no window, nothing. The line responsible:
dir=$(jq -r 'select(.cwd) | .cwd' "$transcript" 2>/dev/null | head -1)
On a big transcript, head -1 reads one line and exits, jq gets SIGPIPE trying to write the second, and with set -o pipefail and set -e on, bash takes that as the whole script failing. Quietly. The fix is to let jq stop itself:
dir=$(jq -rn 'first(inputs | select(.cwd) | .cwd)' "$transcript" 2>/dev/null || true)
What it can't do
It only knows about Terminal.app. No iTerm, no tmux. Sessions are per-machine too: if the conversation happened on my other Mac, cjump says so rather than pretending, but it can't fetch it.
There are all kinds of ways this could be slicker, fancier and all-round better. It could be a proper Rust binary with an fzf picker, installed by Ansible, aware of every terminal emulator ever made. It could be a Claude Code plugin. It could live in a menu bar.
But sometimes all you need is a good old bash script. This one took six minutes to write and saves me a dozen-window rummage every day. And now, when the Claude I'm talking to needs to ask a Claude I closed last week what it was thinking, the dead are only five seconds away.
Martin Harris connects cloud infrastructure to the real world. He has more Terminal windows open than he has cats, and he has six cats.
If you find this blog useful, please consider a donation.
