# How I Built a Skill That Lets Me Talk to Claude's Conversation Memory



When I work with Claude Code on complex projects, I often remember discussing a problem or solution but can't find it. "We fixed that EMFILE error last week, what was the solution?" or "What did we work on yesterday?"

Claude Code stores every session locally. But Claude itself can't search those files by default. So I built a skill that lets Claude search its own conversation history.

This turns Claude into a persistent coding partner that actually remembers past solutions.

<Aside type="tip" title="New to Claude Code Skills?">
If you're not familiar with how skills work in Claude Code, check out my <InternalLink slug="claude-code-customization-guide-claudemd-skills-subagents">guide to CLAUDE.md, skills, and subagents</InternalLink> first.
</Aside>

<Aside type="note" title="Just Want the Code?">
If you just want to check out the skill, find it here: [conversation-search skill](https://github.com/alexanderop/dotfiles/tree/main/claude/skills/conversation-search)
</Aside>

## How Claude Code Stores Conversations

Every Claude Code session gets saved as a JSONL file in `~/.claude/projects/`. The directory structure looks like this:

<FileTree tree={[
  { name: ".claude", open: true, children: [
    { name: "projects", open: true, children: [
      { name: "-Users-alex-Projects-myapp", open: true,comment: "// encoded path", children: [
        { name: "a1b2c3d4.jsonl", comment: "// session file" , open: true},
        { name: "b2c3d4e5.jsonl" },
        { name: "c3d4e5f6.jsonl" },
        { name: "e5f6g7h8.jsonl" },
        { name: "agent-xyz123.jsonl", comment: "// subagent session" }
      ]},
      { name: "-Users-alex-Projects-blog", children: [
        { name: "i9j0k1l2.jsonl" }
      ]}
    ]}
  ]}
]} />

The path encoding is simple: replace `/` with `-` and prefix absolute paths with `-`. So `/Users/alex/Projects/myapp` becomes `-Users-alex-Projects-myapp`.

Each JSONL file contains one JSON object per line:

```json
{"type": "user", "timestamp": "2026-01-16T10:30:00Z", "gitBranch": "main", "message": {"content": "Fix the EMFILE error"}}
{"type": "assistant", "timestamp": "2026-01-16T10:30:15Z", "message": {"content": [{"type": "text", "text": "Let me investigate..."}, {"type": "tool_use", "name": "Bash", "input": {"command": "ulimit -n"}}]}}
{"type": "summary", "summary": "Fixed EMFILE error by increasing file descriptor limit"}
```

Each entry includes the role, timestamp, git branch, message content, and tool uses. The `summary` type appears when Claude generates a conversation summary.

## The Skill Structure

The skill lives in `~/.claude/skills/conversation-search/` with two files:

<FileTree open={true} tree={[
  { name: "conversation-search", open: true, children: [
    { name: "SKILL.md", comment: "// trigger patterns and usage" },
    { name: "scripts", open: true, children: [
      { name: "search_history.py", comment: "// the search engine" }
    ]}
  ]}
]} />

The `SKILL.md` file tells Claude when to activate this skill:

```yaml
---
name: conversation-search
description: Search past Claude Code conversation history. Use when asked to recall,
  find, or search for anything from previous conversations. Triggers include
  "what did we do today", "how did we fix X", "search history", "recall when we"...
---
```

When I ask "what did we do yesterday?", Claude recognizes the trigger and knows to use this skill.

## How the Python Script Works

The script has two modes: **digest** for daily summaries and **search** for finding specific solutions.

### Data Structures

The script parses JSONL files into clean dataclasses:

```python
@dataclass
class Message:
    uuid: str
    parent_uuid: Optional[str]
    role: str  # 'user', 'assistant'
    content: str
    timestamp: str
    tool_uses: list
    tool_results: list

@dataclass
class Conversation:
    session_id: str
    file_path: str
    summary: Optional[str]
    messages: list
    project_path: str
    git_branch: Optional[str]
    timestamp: str

@dataclass
class SearchResult:
    conversation: Conversation
    score: float
    matched_messages: list
    problem_excerpt: str
    solution_excerpt: str
    commands_run: list
```

### Relevance Scoring

The search algorithm tokenizes the query and content, then calculates relevance scores with weighted boosts:

```python
def calculate_relevance_score(query: str, conversation: Conversation) -> tuple:
    query_tokens = tokenize(query)
    total_score = 0.0
    matched_messages = []

    # Summary gets highest weight (3x)
    if conversation.summary:
        summary_tokens = tokenize(conversation.summary)
        summary_overlap = len(query_tokens & summary_tokens) / len(query_tokens)
        total_score += summary_overlap * 3.0

    # Check each message
    for msg in conversation.messages:
        msg_tokens = tokenize(msg.content)
        overlap = len(query_tokens & msg_tokens)

        if overlap > 0:
            msg_score = overlap / len(query_tokens)

            # User messages get 1.5x boost (problem descriptions)
            if msg.role == 'user':
                msg_score *= 1.5

            # Messages with tool uses get 1.3x boost (solutions)
            if msg.tool_uses:
                msg_score *= 1.3

            total_score += msg_score
            matched_messages.append(msg)

    return total_score, matched_messages
```

The weighting makes sense: summaries are the most relevant since they capture the essence. User messages describe problems. Tool uses indicate actual solutions.

### Date Filtering

The script supports filtering by date range:

```bash