I built my own Claude Code interface, because the transcripts were the asset and I kept losing them
Four months of AI coding sessions is 396MB of JSONL nobody can read. I turned it into a searchable record, put a live board over it, and gave the whole thing an agent that can act on what it finds.
I do most of my engineering through AI coding sessions, across several client projects at once. After four months that had produced 548 session transcripts, several hundred megabytes of JSONL, and a recurring problem: every decision I had made, every dead end I had already explored, was written down in a format nothing could read.
Compaction makes it worse. A long session summarises itself to keep going, and the specifics go first. I was re-deciding things I had already decided two months earlier.
So I built the interface I wanted. Here is the architecture and the parts that were not obvious.
The record
Transcripts live in ~/.claude/projects/<project>/<sessionId>.jsonl, one JSON object per line. Parsing them is straightforward. Finding all of them is not, and several of my ingestion bugs were silent rather than loud.
Loaded, that is 548 files, 105,336 records, 2,047 work episodes, 16 million characters, roughly 4 million tokens, zero unparseable lines.
It goes into Postgres 17 with pgvector and pg_trgm, embedded locally through Ollama. Retrieval runs three lanes fused with reciprocal rank fusion, because semantic search finds what you meant and misses what you typed. The ranking and the ingestion traps are their own subject, and I wrote them up separately in the note on hybrid search.
Redaction runs at load time, before anything is stored. Nine regex families catch API keys, connection strings and tokens, each replaced with a stable placeholder derived from a hash, so the same key always renders identically and you can still see that two sessions shared a credential without the credential being present.
The board, and why nothing on it is typed
The first version of the interface was a timeline of everything I had ever done. It looked impressive and I did not use it once.
The diagnosis, when I went looking for it, was that 2,047 episodes over four months is about seventeen events a day. By the EEMUA 191 standard for alarm systems, six alarms an hour is a busy console. Nothing on that screen could change while I was looking at it. Static history is a query corpus, and a query corpus deserves zero pixels at rest.
What replaced it is seven fixed rows, one per project, and every value is derived:
- RUNNING comes from the process table:
pgrep claude, thenlsoffor its working directory. File modification time alone was wrong in both directions, marking a project idle while a session was mid-turn, and missing running work in projects with no history. - DIRTY comes from
git statusin the project's real directory. - COLD comes from episode recency.
No status field anyone maintains by hand, because state a human maintains is wrong within a week. It earned its place on day one by telling me about 51 uncommitted files on a branch I had forgotten and 443 on another repository's main.
Idle rows sit at 30% opacity, so anything lit is asking for something. That is borrowed from the Airbus dark-cockpit convention, and it is the difference between a dashboard and an instrument.
The agent
The first version answered questions by retrieving twelve past sessions and summarising them. It could describe the past and do nothing about the present.
Replacing that endpoint with the Claude Agent SDK took an afternoon and changed what the product is. It runs the same loop as Claude Code, with Read, Edit, Bash, Grep and WebSearch streamed to the screen as they happen.
Two of those options carry most of the value:
options: {
cwd: selectedProject,
additionalDirectories: [process.env.HOME!, ...projects],
settingSources: ["user", "project"],
systemPrompt: { type: "preset", preset: "claude_code", append: APPEND },
}
settingSources picks up the MCP servers already registered on the machine, including the one I wrote over the record itself. Recall became a tool the model chooses to call rather than context forced in front of every question, which is what retrieval should always have been.
cwd was the one I got wrong, twice. Treating it as a boundary rather than a starting point meant the agent told me it had no access to a website whose source was one directory over on the same laptop. A directory the model does not know about is a directory it will not look in, so the prompt now names them.
Voice, both directions
Dictation runs through whisper-stream with small.en over server-sent events, about 400ms behind speech, spawned per keypress so the microphone is never held open between utterances. Replies are read back by Kokoro, held resident so no sentence pays the 2.5-second model load, at 420 to 850ms per sentence.
Both are local. No audio and no transcript leaves the machine, which was the point.
What it costs and what it replaced
A three-turn answer that reads two files runs about twelve cents. A follow-up inside the same session, answering from its own previous turn, costs one.
Ingestion refreshes every three minutes while the interface is open, because a memory that stops at the last manual import is a snapshot. I learned that when the thing spent twenty-three cents and a subagent trying to answer a question whose answer was in a session it had never been shown.
The useful lesson is smaller than the build. Almost every hard problem was about derivation rather than generation: deriving state instead of storing it, deriving the file list from two sources because neither was complete, deriving the end of a spoken sentence from silence. The model was rarely the difficult part.
Want this built instead of read?
I build the internal apps, automations and integrations described in these notes. Whether you need a week of it or a full-time engineer — scoped up front and documented as I go, so you own it afterwards.