Skip to content

About

Semantic search over your Claude Code session history, plus a live view of which sessions are actually running. Local embeddings + BM25, real AI-generated titles, pinning, and open-or-reveal in VS Code. No API key, nothing leaves your machine.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

Repository files navigation

claude-sessions

Semantic search over your Claude Code session history, plus a live view of which sessions are actually running.

If you use Claude Code heavily, you have probably hit these:

  • You remember a conversation but not its title, and the picker's search can't find it.
  • Half your sessions are named push it, yes, implement, or Review RFC in detail — even when the session went somewhere else entirely.
  • You have five Claude tabs open and no idea which one is still working.

This is a local sidecar that fixes all three. No API key, no cloud, nothing leaves your machine.

Search by meaning. The struck-through line is what the built-in picker would have shown for the same session:

search

See which sessions are actually working, with real titles instead of hash names (note the duplicate webapp-a1 the built-in view cannot disambiguate):

running

Screenshots use a synthetic demo history, not real sessions.


Why the built-in search misses things

All three problems have a specific cause in the extension bundle (measured against a real 500+ session, 450 MB history on v2.1.220):

1. Titles are generated once and never refreshed. Title precedence is customTitle → aiTitle → lastPrompt → summaryHint → firstPrompt. The ai-title record is derived from the opening prompt, then rewritten byte-identically forever. In the corpus measured, 104 sessions had more than 5 title records but only 1 distinct value — including a 1200-message session still labelled from its first line. Another 132 sessions had no ai-title at all, so the picker fell back to raw prompt text.

2. Session search is a substring match on the title. Fuse.js is bundled — but wired exclusively to file @-mentions (keys:[{path},{filename}]). Session search is title.toLowerCase().includes(query). There is no body index and no embedding code anywhere in the bundle, so searching charts cannot find a session that only ever said dataviz.

3. Running sessions get hash names. ~/.claude/sessions/<pid>.json labels sessions nameSource:"derived" — myrepo-93, myrepo-d1, myrepo-5d. Two concurrent sessions can collide on the same derived name. And lockfile mtime is not an activity signal: live PIDs were observed with lockfile mtimes 258, 275 and 141 minutes stale.

What this does instead

built-in claude-sessions
search scope session title only every prompt + assistant message
matching substring embeddings + BM25 + synonyms
titles first prompt, never updated real summaries of the whole session
running sessions myrepo-d1 actual title, live state, current tool
pinning — yes
open a result — opens/reveals the tab in VS Code

Install

Requires macOS or Linux, Python 3.10+, and the claude CLI (for title generation only — search works without it).

git clone https://github.com/BilalAtique/claude-sessions.git
cd claude-sessions
./install.sh

install.sh creates a venv, installs deps, prefetches the embedding model, builds the index, and symlinks claude-sessions into ~/.local/bin.

Heads-up: the venv is ~1.1 GB because sentence-transformers pulls in PyTorch. That is the price of fully-local embeddings with no API key.

Then generate real titles (optional, but it is most of the value):

claude-sessions retitle          # ~11s per session, resumable, cached

Always-on (macOS)

./scripts/install-launchd.sh     # server at login + hourly reindex
./scripts/uninstall-launchd.sh   # undo

Two agents: the web UI with KeepAlive (restarts if it crashes), and an hourly index + retitle refresh. Logs land in the data dir.

Use it

claude-sessions serve            # http://127.0.0.1:7788

Web UI — two tabs. search history ranks by meaning and strikes through the title the picker would have shown when it differs. running auto-refreshes every 5s with real titles, working/idle/stale state, the tool each session is executing, and a badge on duplicate derived names.

CLI

claude-sessions search "the chart thing"
claude-sessions search "why was the build slow" -n 5 --project myrepo
claude-sessions live --watch          # running sessions, refreshing
claude-sessions open 3f9a1c           # open/reveal in VS Code (id prefix ok)
claude-sessions pin 3f9a1c -m "note"  # pin / unpin / pins
claude-sessions index                 # incremental; --full to rebuild
claude-sessions retitle --limit 40
claude-sessions status

Open in VS Code

Click open ↗ on any row, or claude-sessions open <id>.

This uses a URI route the extension already registers:

vscode://Anthropic.claude-code/open?session=<id>&prompt=<text>

It routes to claude-vscode.primaryEditor.open → createPanel(sessionId, …), which keeps a sessionPanels map and calls panel.reveal() when that session already has a panel. So an already-open tab is focused rather than duplicated, including when it is inactive or in another editor group.

The UI calls /api/open/<id> rather than linking vscode:// directly: a raw link triggers a browser "open external app?" prompt every time, and is blocked outright inside the VS Code Simple Browser.

Four tests assert the extension still exposes this route, so an update that removes it fails loudly instead of silently breaking the button.

VS Code tab

Add to keybindings.json to open the UI in an editor tab:

{ "key": "cmd+shift+h", "command": "simpleBrowser.show",
  "args": "http://127.0.0.1:7788" }

How search works

Hybrid, because neither signal alone is sufficient — both limits were measured, not assumed:

  • Dense — all-MiniLM-L6-v2 (384-dim), fully local. Handles paraphrase, but cosine("the chart thing", "dataviz rendering") = 0.18, so it alone misses domain jargon.
  • Lexical — SQLite FTS5 + BM25 over message bodies, with a synonym map (chart → dataviz, recharts, plot…). Catches the exact jargon the sentence model does not know.
  • Title/keyword bonus — small boost for hits in title, branch, project or extracted keywords.

Score = 1.0·dense + 0.6·lexical + 0.35·title. Pinned sessions sort first.

Isolating the signals on one query shows why both are needed: dense-only ranked an unrelated session first; lexical-only missed the paraphrase; the fusion got it right.

Performance

Vectors are cached in-process as one (n_chunks × 384) float32 matrix and scored with a single BLAS matmul.

pure-Python cosine numpy matmul
8.6k chunks 950 ms —
17.3k chunks ~1.9 s (extrapolated) 40–100 ms
200k chunks ~22 s (extrapolated) ~1 s

numpy ships with PyTorch, so this added no dependency. The model is preloaded at server startup; the first cold query otherwise costs ~11 s.

Coverage

MAX_CHUNKS_PER_SESSION = 250, CHUNK_CHARS = 900. Both were originally wrong in ways worth documenting:

  • At a cap of 60, only 31% of turns were indexed and 77 sessions were truncated — the longest ones, which are exactly the hardest to recall. At 250: 65% coverage, 29 still capped.
  • CHUNK_CHARS was 1200 (~300 tokens), but MiniLM's max_seq_length is 256. The model silently dropped the tail of every chunk. FTS5 still indexed the full text, which masked the bug entirely. 900 chars fits the window.

Assistant turns are sampled evenly across the whole session rather than from the head, so a long session stays findable by something in its middle. Turns under 40 chars are filtered before sampling — filtering after left the budget under-filled.

How titles are fixed

Three layers, highest precedence first:

  1. custom_title — a manual rename always wins, never overwritten.
  2. llm_title — a real summary from the local claude CLI (--model haiku). No API key, no extra cost beyond your existing plan. This is the only layer that can summarize rather than quote:
    • Review RFC in detail → ABC-1234 ledger completeness subtasks
    • yes, lets do it, → XYZ-567 opening balance sync recovery
    • push it → Agent chart rendering fix deployed and scheduled
  3. derived_title — extractive fallback: first substantive prompt, skipping bare acknowledgements (yes, push it, check now).

Cached in llm_titles keyed by transcript mtime, so a session is re-summarized only after it changes. Re-running retitle is cheap and resumable.

These titles do not change what the VS Code picker renders — the extension is a 2.6 MB minified bundle with no plugin surface for its session list, so it will keep showing push it. This is a parallel surface that hands off into the extension.

Security

The server binds to loopback, but that alone is not sufficient:

  • Token auth on every /api route. Any local process — including a malicious npm postinstall — could otherwise read your whole transcript history over 127.0.0.1. The token is generated once into <data>/api-token with 0600 perms and injected into the served page (same-origin), so the UI stays zero-config.
  • Host-header validation. A web page you visit can reach a loopback port via DNS rebinding, so any Host outside a localhost allowlist gets 421.
TOK=$(cat "$HOME/Library/Application Support/claude-sessions/api-token")
curl -H "X-Api-Token: $TOK" "http://127.0.0.1:7788/api/search?q=charts"

Transcripts contain source code, command output and whatever else you pasted. Never expose this on a public interface.

Parsing notes

Session transcripts are messier than they look. All of these were hit in practice:

  • message.content is sometimes a string, sometimes a list of blocks.
  • The large majority of role:"user" records are tool results, not prompts (~4281 of 4915 in one sample). Indexing them buries the human signal.
  • User-role records also carry harness plumbing: <task-notification>, <system-reminder>, The user opened the file …, bare toolu_… IDs. An early version leaked these into titles, so is_injected_context() rejects them and is shared by the indexer and the live view.
  • Live transcripts can end mid-line; the parser tolerates truncated JSON.

Live-session detection

  1. Read each ~/.claude/sessions/<pid>.json.
  2. os.kill(pid, 0) for real liveness — dead PIDs dropped.
  3. Resolve the title from the transcript, not the derived name.
  4. Derive activity from the transcript tail. Lockfile mtime is unusable: observed 481 min stale on a process whose transcript was 350 min old.
  5. Sample CPU time twice (~0.6 s apart) to catch a session that is computing but has not flushed output.

working = transcript written in the last 90 s, idle = last 15 min, else stale.

Layout

Code and data are separate — the venv is ~1.1 GB and the index grows, so neither belongs in the repo.

what where
code this repo
index, venv, logs, token ~/Library/Application Support/claude-sessions/
CLI ~/.local/bin/claude-sessions
env var overrides
CLAUDE_SESSIONS_HOME data dir (index, venv, logs, token)
CLAUDE_SESSIONS_DB index path only
CLAUDE_SESSIONS_CLAUDE_DIR where to read history from (default ~/.claude)

The last one is how the demo screenshots are generated against a synthetic history instead of a real one.

Tests

"$HOME/Library/Application Support/claude-sessions/venv/bin/python" test_csindex.py

104 tests: unit coverage of the plumbing filters and title cleaners; a synthetic ~/.claude/projects tree exercising indexing, search, incremental reindex and live detection against known-correct expectations; auth and DNS-rebinding checks; assertions that the VS Code URI route still exists; and checks against the real local index (no plumbing in titles, every session titled, query latency, live titles never equal to the derived hash).

Files

path role
csindex/parse.py transcript → SessionMeta + turns; plumbing filters
csindex/index.py chunking, embedding, extractive titles, incremental reindex
csindex/search.py hybrid dense + BM25 + synonym expansion
csindex/retitle.py summary titles via the local claude CLI
csindex/live.py running-session detection, activity state, name collisions
csindex/server.py FastAPI JSON API + web UI + auth
csindex/cli.py command line entry point

Caveats

  • macOS-first. The auto-start is launchd. Linux/Windows branches exist for the open command but are untested.
  • 1.1 GB install for a 384-dim model, courtesy of PyTorch.
  • Titles cost a claude call each (~11 s). The hourly agent does 60.
  • The synonym map is hand-authored — it only knows the jargon in it.
  • Tied to the current transcript layout under ~/.claude/; a format change upstream could require parser updates.

License

MIT

About

Semantic search over your Claude Code session history, plus a live view of which sessions are actually running. Local embeddings + BM25, real AI-generated titles, pinning, and open-or-reveal in VS Code. No API key, nothing leaves your machine.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages