chat-archeologist — retrieve context from prior Claude Code sessions
Sessions persist as JSONL files at ~/.claude/projects/<project-slug>/<session-uuid>.jsonl. Each line is one event: user prompts, assistant responses (text + thinking blocks), tool calls, tool results, hook injections. A large local session archive (files range from kilobytes to hundreds of MB).
Two layers of memory exist locally:
| Layer | What it holds | When to read |
|---|---|---|
~/.claude/projects/<project>/memory/MEMORY.md + project_*.md |
Curated outcomes, decisions, validated findings | First — faster, more reliable |
~/.claude/projects/<project>/<uuid>.jsonl |
Reasoning traces, rejected alternatives, partial understandings | Fall back here when the curated layer doesn't cover the question |
This skill drives the second layer.
Helpers (already installed)
~/.claude/scripts/chat-search.sh "<query>" [max_per_file=3] [total_cap=30]
Ripgrep across all session JSONLs, sorted by file mtime descending (most recent first). Returns <filepath>:<lineno>:<snippet> lines. Search is fixed-string + case-insensitive by default.
~/.claude/scripts/chat-search.sh "p95 latency regression" 3 10
~/.claude/scripts/chat-search.sh "match_named_modules"
~/.claude/scripts/chat-extract.py <filepath> <lineno> [--max-chars 5000]
Extracts the user/assistant turn pair around a matched line as readable markdown. Walks backward from the match to find the most recent user message with text content (not tool_result placeholders), walks forward to find the next assistant message with text content (not tool_use placeholders). Skips thinking blocks. UTF-8-safe on Windows.
python ~/.claude/scripts/chat-extract.py "/c/Users/you/.claude/projects/<project>/<uuid>.jsonl" 8076 --max-chars 1500
Workflow (every retrieval)
Parse the question. Identify 2-4 distinctive keyword candidates. Prefer specific tokens (function names, error messages, paper section numbers, exact figures like "420ms", "3.2x", commit hashes) over generic terms ("the result", "the issue").
Search. Run
chat-search.shwith your strongest keyword first. Cap at 4 search calls — past 4 you're hunting blind. If 0 hits, try a different keyword. If >30 hits, narrow with a more distinctive phrase.Triage. Most recent + highest-keyword-density hits go to the top of the read queue. Aim for 3-6 turn pairs total across the synthesis.
Extract. For each priority hit, run
chat-extract.py <filepath> <lineno>. Read the user prompt + assistant response.Synthesize. Answer the question with concrete content from the extracted turns. Cite each load-bearing claim as
<session-uuid-prefix-8>:L<lineno>so the source can be re-located vials ~/.claude/projects/*/<prefix>*.
Hard rules
- Never fabricate content from sessions you didn't actually extract. If search returns 0 hits, say so — don't invent a plausible answer.
- Don't read full JSONLs. Some are 220 MB. Always go through
chat-extract.pywhich streams + bounds output. - Quote load-bearing numbers exactly. Don't paraphrase "p95 420ms vs 180ms baseline" into "roughly 2x slower." Numbers are where paraphrasing introduces errors.
- Skip thinking blocks in citations. The Python helper strips them; if raw thinking content surfaces anyway, don't quote it.
- Check curated memory first. If MEMORY.md or a project_*.md entry already has the answer, use that — it's faster and authoritative. Only fall back to chat archeology when the curated layer doesn't cover the question.
Citation format
- Good: "The retry timeout was bumped from 5s to 30s after the staging 504s in session
11bce930:L47288" — has session prefix + line, allows re-verification. - Bad: "I think we discussed this somewhere in May" — no citation, useless to verify.
- Bad: "The user said the result was a failure" — no quote, no line, ungroundable.
The 8-character session UUID prefix is enough to re-locate the file. Different sessions in the same project will have different prefixes — they don't collide.
When to refuse / suggest a different path
- Question is about current state (running pods, in-flight build, current branch) → read state directly, don't search history.
- Question is about future plans → read paper §14 work queue or recent CLAUDE.md status block.
- Question is so vague no keywords distinguish it ("what have we done") → ask the requester for a specific decision, error, finding, or date range.
- Question is about a curated decision likely in MEMORY.md → grep that first; chat archeology is the backup, not the primary.
Output format
Lead with a 1-2 sentence direct answer if your extraction supports one. Then:
Citations:
<session-prefix>:L<lineno>— what was said at that turn (1 sentence summary)- (repeat for up to 4 most load-bearing sources)
What you didn't find (if relevant): brief note on what the search couldn't surface, so the requester knows where retrieval stopped being reliable.
If multiple sessions disagree (e.g., one declared a result ALIVE, a later one retracted it), note both and order by mtime — the latest session is usually the current truth, but only after verifying the framing actually evolved (vs the older session being a strawman).
Promotion path back to memory
When you surface a load-bearing finding from chat archeology that isn't yet captured in MEMORY.md or any project_*.md entry, flag the gap:
"This came from chat archeology, not durable memory — worth a one-line MEMORY.md entry pointing to a new
project_*.mdso future-you doesn't have to re-search this."
Don't write the file yourself unless asked. Just flag the gap. The point of this skill is to make the chat archive a queryable layer of memory, not to replace the curated layer.
Subagent vs skill — when to use which
| Path | Tool | When |
|---|---|---|
| Skill (this file) | Direct helper invocation in current session | Fast lookup mid-conversation; current session needs the data inline to continue reasoning |
Subagent (~/.claude/agents/chat-archeologist.md) |
Agent({ subagent_type: "chat-archeologist", ... }) |
Fresh-context dispatch; the synthesized answer IS the deliverable; current session shouldn't be polluted with raw search output |
Pick the subagent when retrieval-and-synthesis is the entire task. Pick this skill when the current session needs prior context to keep reasoning forward.
UX limitation worth knowing
The default snippet from chat-search.sh is 120 chars of raw JSONL, which often shows JSON metadata (parentUuid, promptId, etc.) before the actual content starts. Triaging 30 hits by snippet alone is slow. Workaround: extract the top 3-5 hits regardless of snippet quality and let chat-extract.py show you the actual turn — the snippet is for ordering, the extract is for reading.
A v2 of the search script could pre-extract the text field via Python so snippets are prose, not JSON. ~10 LOC change. Not blocking.