---
title: "Raising Claude From the Dead With a Bash Script"
date: "2026-10-18"
publish_date: "2026-10-27"
excerpt: "I have a dozen Claude Code windows open at once and a memory like a sieve. cjump finds the conversation I'm thinking of, brings its window to the front if it's still open and resurrects it if it isn't, so one Claude can go and ask another what it was thinking."
tags: ["claude-code", "bash", "macos", "applescript", "conversation-memory"]
author: "Martin Harris"
canonical: "https://blog.nakomis.com/2026-10-18-raising-claude-from-the-dead"
repos:
  - name: "claude-chats"
    url: "https://github.com/nakomis/claude-chats"
  - name: "mcps"
    url: "https://github.com/nakomis/mcps"
approved: true
---

# Raising Claude From the Dead With a Bash Script

![A Victorian séance table lit by a single candle, where instead of a crystal ball a glowing terminal window floats above the table, surrounded by a ring of smaller dimmer terminal windows hovering in the dark](images/raising-claude-from-the-dead-1.png)

A while ago I wrote [Claude Chats](https://blog.nakomis.com/2026-05-15-giving-claude-a-persistent-memory). 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-locally` from 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.

![Two vintage brass telephones on a dark wooden desk connected by a single glowing cable, one telephone sitting in warm candlelight and the other half covered in cobwebs and dust as if newly woken](images/raising-claude-from-the-dead-2.png)

## 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 `cd` to the right directory and launch `claude --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](https://github.com/nakomis/claude-chats/blob/main/scripts/cjump).

```bash
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

```mermaid
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:

```bash
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 `cd`s there and `exec`s 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 -p` starting 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.

![A stopwatch lying on a workbench next to a tangle of unplugged cables and adapters that have just been removed from a small sleek device, lit by a single desk lamp](images/raising-claude-from-the-dead-3.png)

## 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:

```bash
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:

```bash
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.*


{{donate}}
