Explore with AI
ChatGPTClaudeGeminiPerplexity
January 14, 202610 minute read

I Built a Claude Code Skill to Search My Past Sessions

Mike Ritchie
Building a Claude Code Skill to Search Past Sessions | Definite

The built-in /resume command in Claude Code only searches conversation titles. That's fine until you remember discussing something three weeks ago and need to find where.

I kept running into this. "Where did I debug that Kubernetes OOM issue?" "Which session had that SQL query for cohort analysis?" The title search wasn't cutting it.

The Fix

Claude Code stores your conversations locally as JSONL files in ~/.claude/projects/. Each project gets a folder, each session gets a file. The data is right there.

So I wrote a Python script that:

  • Searches all session files with regex support
  • Shows matching snippets with context
  • Sorts by most recent first
  • Outputs the claude --resume <id> command so you can jump back in

Now I run /cc-search airflow and get every session where I mentioned Airflow:

═══ 2026-01-12 14:32 ═══
Project: Users/mritchie712/mike3
Session: abc123-def456
Resume:  claude --resume abc123-def456

  👤 ...the Airflow DAG is failing on the dbt task...

Small friction → small script → big time savings.

The Full Script

Here's the complete search.py:

#!/usr/bin/env python3
"""Full-text search across Claude Code conversation history."""

import json
import re
from datetime import datetime
from pathlib import Path

CLAUDE_PROJECTS = Path.home() / ".claude" / "projects"


def extract_text(content) -> str:
    """Extract searchable text from message content."""
    if isinstance(content, str):
        return content
    if isinstance(content, list):
        texts = []
        for item in content:
            if isinstance(item, dict):
                if item.get("type") == "text":
                    texts.append(item.get("text", ""))
                elif item.get("type") == "tool_result":
                    texts.append(str(item.get("content", "")))
            elif isinstance(item, str):
                texts.append(item)
        return "\n".join(texts)
    return ""


def search_session(session_path: Path, pattern: re.Pattern, context_lines: int = 2) -> list:
    """Search a single session file for matches."""
    results = []
    try:
        with open(session_path, "r") as f:
            messages = []
            for line in f:
                try:
                    entry = json.loads(line)
                    if entry.get("type") in ("user", "assistant"):
                        text = extract_text(entry.get("message", {}).get("content", entry.get("message", "")))
                        messages.append({
                            "role": entry.get("type"),
                            "text": text,
                        })
                except json.JSONDecodeError:
                    continue

            for i, msg in enumerate(messages):
                if pattern.search(msg["text"]):
                    # Find match snippet
                    match = pattern.search(msg["text"])
                    if match:
                        start_pos = max(0, match.start() - 100)
                        end_pos = min(len(msg["text"]), match.end() + 100)
                        snippet = msg["text"][start_pos:end_pos]
                        if start_pos > 0:
                            snippet = "..." + snippet
                        if end_pos < len(msg["text"]):
                            snippet = snippet + "..."

                    results.append({
                        "message_index": i,
                        "role": msg["role"],
                        "snippet": snippet,
                    })
    except Exception:
        pass
    return results


def search_all(query: str, project_filter: str = None, limit: int = 20) -> None:
    """Search all sessions for a query."""
    pattern = re.compile(query, re.IGNORECASE)
    sessions_with_matches = []

    for project_dir in CLAUDE_PROJECTS.iterdir():
        if not project_dir.is_dir():
            continue

        project_name = project_dir.name.replace("-", "/")
        if project_filter and project_filter.lower() not in project_name.lower():
            continue

        for session_file in project_dir.glob("*.jsonl"):
            if session_file.name.startswith("agent-"):
                continue  # Skip agent sub-sessions

            results = search_session(session_file, pattern)
            if results:
                mtime = datetime.fromtimestamp(session_file.stat().st_mtime)
                sessions_with_matches.append({
                    "project": project_name,
                    "session_id": session_file.stem,
                    "modified": mtime,
                    "matches": results,
                })

    # Sort by modification time (most recent first)
    sessions_with_matches.sort(key=lambda x: x["modified"], reverse=True)

    # Print results
    print(f"\n🔍 Found {sum(len(s['matches']) for s in sessions_with_matches)} matches in {len(sessions_with_matches)} sessions\n")

    shown = 0
    for session in sessions_with_matches:
        if shown >= limit:
            remaining = len(sessions_with_matches) - shown
            if remaining > 0:
                print(f"\n... and {remaining} more sessions (use --limit to see more)")
            break

        print(f"═══ {session['modified'].strftime('%Y-%m-%d %H:%M')} ═══")
        print(f"Project: {session['project']}")
        print(f"Session: {session['session_id']}")
        print(f"Resume:  claude --resume {session['session_id']}")
        print()

        for match in session["matches"][:3]:  # Show max 3 matches per session
            role_icon = "👤" if match["role"] == "user" else "🤖"
            print(f"  {role_icon} {match['snippet'][:200]}")
            print()

        if len(session["matches"]) > 3:
            print(f"  ... and {len(session['matches']) - 3} more matches in this session")

        print()
        shown += 1


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Search Claude Code conversation history")
    parser.add_argument("query", help="Search query (regex supported)")
    parser.add_argument("--project", "-p", help="Filter by project path substring")
    parser.add_argument("--limit", "-l", type=int, default=20, help="Max sessions to show")

    args = parser.parse_args()
    search_all(args.query, args.project, args.limit)

Setting Up the Skill

To make this a skill Claude can invoke:

  1. Create the folder structure:
.claude/skills/cc-search/
├── SKILL.md
└── scripts/
    └── search.py
  1. Add the SKILL.md:
---
name: cc-search
description: Full-text search across Claude Code conversation history.
  Use when searching past conversations, finding previous discussions,
  or looking up what was discussed before.
---

# Claude Code Conversation Search

## Usage

python .claude/skills/cc-search/scripts/search.py "search term"

# Filter by project
python .claude/skills/cc-search/scripts/search.py "query" --project myproject

# Show more results
python .claude/skills/cc-search/scripts/search.py "query" --limit 50

The description in the frontmatter tells Claude when to invoke the skill. The rest is documentation it can reference.

Try It

If you use Claude Code regularly, look for the small annoyances. The ones you work around five times a day. Those are your skill candidates.

The investment is minimal. The payoff compounds.

Data doesn't need to be so hard

Get the new standard in analytics. Sign up below or get in touch and we'll set you up in under 30 minutes.