sessiongrep: A Local-First Memory Layer for CLI Agents
SQLite + FTS5 turns raw Claude Code and Codex CLI transcripts into something humans and agents can actually search.
Member of Technical Staff @ Brain Co
SQLite + FTS5 turns raw Claude Code and Codex CLI transcripts into something humans and agents can actually search.
Member of Technical Staff @ Brain Co
You solved that bug last week. Your next agent session has no idea.
If you use Claude Code, Codex CLI, or Cursor every day, you know the failure mode. The prompt that finally worked, the migration path you ruled out, and the mental model you built over two hours still exist on disk, but they are buried inside provider-specific JSONL files with opaque names. Humans don't want to read them. Agents don't know how to retrieve them. So both end up repeating work.
We built sessiongrep to fix that: a small, fast Rust CLI that turns your scattered local session history into something you — and your agent — can actually search.
Two frictions compound.
The first is human recall. Claude Code writes sessions under ~/.claude/projects/**/*.jsonl. Codex CLI writes them under ~/.codex/sessions/**/*.jsonl, with extra metadata in ~/.codex/session_index.jsonl and ~/.codex/state_5.sqlite. Cursor keeps its own transcripts under ~/.cursor/projects/**. The filenames are opaque. The formats differ. A simple question like "where was that Redis migration?" turns into a slow walk through hashed directories and noisy JSONL. The built-in resume pickers don't save you either — Claude's often shows /exit as the last prompt, which is the one line that tells you nothing.
The second is agent recall. Each new session starts cold. The context you earned yesterday, the file layout you mapped, the dead-end you ruled out, the user preference you finally discovered are invisible to today's agent. It asks the same questions, rereads the same files, and proposes the same failed approach.
The information is not missing. It is stranded.
grep over JSONL is noisy. Matches land inside tool payloads, bookkeeping messages, and long blobs of machine output.The right primitive here is retrieval, not reinvention.
Session files are already structured enough to index well. They are append-only JSONL with stable provider IDs, timestamps, and a predictable shape. That is enough to build a derived index that is:
mtime and file sizeThis matters because it keeps the system boring. There is no daemon to babysit, no server to deploy, and no synchronization story to debug. The index lives at ~/.local/share/sessiongrep/index.db. Delete it and the next reindex --full rebuilds it.
sessiongrep ships two binaries: sessiongrep and sessiongrep-mcp. Both read from the same SQLite index.

Providers: Each source system gets its own adapter.
~/.claude/projects/**/*.jsonl.~/.codex/sessions/**/*.jsonl and enriches those records with metadata from session_index.jsonl and state_5.sqlite.~/.cursor/projects/** and normalizes Cursor's thread transcripts into the same shape.Adding a new provider is just another adapter over a new session format.
Normalization: Claude, Codex, and Cursor store different kinds of session artifacts, so sessiongrep collapses all three into one provider-agnostic Session model: ID, provider, title, summary, cwd, repo root, timestamps, preview text, and flattened transcript. Claude and Cursor subagent transcripts are intentionally excluded to avoid duplicate data.
Index: SQLite runs in WAL mode and stores structured session metadata in regular tables plus an FTS5 virtual table over title, summary, preview_text, and transcript_text. Search starts with FTS candidate retrieval, then rescoring combines fuzzy matching and metadata-aware ranking across title, summary, cwd, repo, preview, and transcript. The result is simple: good recall without extra infrastructure.
Incremental refresh: Every read command runs an incremental reindex first. Files whose mtime and size have not changed are skipped. Changed files are reparsed and upserted. In steady state, the refresh cost is small enough to disappear into normal terminal use.
Surfaces: The CLI exposes search, list, show, resume, export, doctor, and paths. The TUI, built on ratatui, gives live search, a preview pane, and one-key resume. The MCP server exposes the same index to agents.
sessiongrep never mutates provider state. sessiongrep resume <id> resolves the session and prints or executes the provider's own command: claude --resume <id> or codex resume <id> (Cursor transcripts are indexed and searchable, but resume isn't currently supported there). That constraint is intentional. sessiongrep is a recall layer, not a replacement runtime.
Why Rust? Because this kind of tool has to disappear into the workflow. A single static binary, fast startup, and low enough latency that searching old work feels cheaper than re-explaining it.
The architectural payoff is not just that humans can search session history. Agents can too.
sessiongrep-mcp exposes four tools over MCP:
search_sessions: Keyword search with optional provider filter and limitget_session: Full transcript by ID, with max_lines to bound contextlist_sessions: Recent sessions, filterable by provider and path prefixget_resume_command: The exact native CLI command needed to resume a sessionInstallation is one line:
claude mcp add --scope user --transport stdio sessiongrep -- sessiongrep-mcp
# or
codex mcp add sessiongrep -- sessiongrep-mcpAfter that, a prompt like: "find my previous session where I was setting up Datadog metrics and pull in the part where we got the agent to emit histograms" becomes self-serve. The agent can call search_sessions, inspect the candidates, then pull only the relevant slice with get_session.
✨ One accidental discovery was that retrieval quickly turns into review. Once an agent can pull a previous session into context, you can ask it to critique that earlier work with fresh eyes. Claude can review a plan drafted in Codex. Codex can poke holes in a migration written up in Claude. A prompt like "find the session where we outlined the Azure identity rollout and review the plan like a skeptical staff engineer; what assumptions are weak, what sequencing is risky, and what production questions did we miss?" is now a single prompt away. sessiongrep does not just help an agent remember what it said before; it gives the old reasoning a second pass.
The goal for sessiongrep was never benchmark theater. The goal was simpler: make recall cheaper than repetition.
On a local test corpus of 149 sessions across Claude Code and Codex CLI, the release binary rebuilt the full index in about 59 ms and returned a Datadog query in about 18 ms. The exact numbers will vary by machine and corpus size, but the important point is that the tool is fast enough to use by default instead of only when you are desperate.

sessiongrep is not a knowledge base. It does not deduplicate concepts across sessions, replace documentation, or decide what mattered for you. It is a recall layer over raw session history.
It is also only as private as the machine it runs on. Session transcripts contain whatever you typed and whatever the agent saw, including API keys, internal hostnames, and partially configured secrets. Keep the index local. Do not check it in. Do not sync it to anything you would not trust with raw shell history.
And do not treat searchable history as a substitute for writing things down. Agents can recover what happened. Humans still need to decide what it means.
There are a few obvious directions from here:
diff_sessions, summarize_session, and timeline_for_repo would let agents reason across history instead of only retrieving it.We would rather grow slowly and keep the tool boring. The best compliment sessiongrep can get is that you stop thinking about it.
Treat retrieved sessions as untrusted input. If a past session ingested a poisoned page or tool output, surfacing it into a new agent context can carry that prompt injection forward. Same risk surface as pasting old transcripts in by hand, but worth naming.