find-claude

Installation
SKILL.md

Find Claude — Conversation History Search

Search and navigate Claude Code conversation histories across all projects. Two-phase: index for fast lookup, grep as fallback for deep content search.

Step 0: Ensure search is ready

Check if the keyword index exists:

test -f ~/.claude/session-index/index.json && echo "INDEX EXISTS" || echo "NO INDEX"

If NO INDEX (first time): run the full setup script. This builds the keyword index and optionally installs semantic search (episodic-memory). Tell the user what's happening — first-time setup takes a few minutes.

bash ~/.claude/skills/find-claude/scripts/setup.sh

If INDEX EXISTS: run an incremental update (<1s):

bun ~/.claude/skills/find-claude/scripts/index-sessions.ts

Step 1: Classify the search

Determine which search flow applies based on the user's request:

User says Flow Primary fields to search
"find the conversation about X" Topic search firstUserMessages, lastUserMessages, continuationSummaries
"where was I on X?" / "pick up where I left off" Resumption lastUserMessages (prioritized), then firstUserMessages
"all conversations about X" Comprehensive All text fields + filesModified + skills, group by project
"what was I working on today/yesterday?" Recency Filter by lastActiveAt date
"conversations where I used /spec" Skill filter skills array
"conversations that touched schema.ts" File filter filesModified array
"conversations about PR #2212" PR filter prs array
"what was I doing on feat/auth?" Branch filter branches array
"conversations in the worktree X" Worktree filter worktrees array
"all conversations touching doltgresql repo" Repo filter repos array
"conversations named 'auth refactor'" Title filter title field
"sessions using the code-reviewer agent" Agent filter agentSetting field

Step 2: Search (keyword + semantic in parallel)

Two search modes available: keyword (default) and semantic (--semantic).

Keyword search (default) — exact string match against indexed fields. The query is matched as-is (not split into individual words).

# Exact string search against index
bun ~/.claude/skills/find-claude/scripts/search.ts "figma hook"

# Filtered searches
bun ~/.claude/skills/find-claude/scripts/search.ts --skill eng:spec "openbolt credential"
bun ~/.claude/skills/find-claude/scripts/search.ts --pr 2212
bun ~/.claude/skills/find-claude/scripts/search.ts --branch feat/auth
bun ~/.claude/skills/find-claude/scripts/search.ts --today
bun ~/.claude/skills/find-claude/scripts/search.ts --file manage-schema.ts
bun ~/.claude/skills/find-claude/scripts/search.ts --worktree agents-pr2212
bun ~/.claude/skills/find-claude/scripts/search.ts --repo doltgresql
bun ~/.claude/skills/find-claude/scripts/search.ts --title "auth refactor"
bun ~/.claude/skills/find-claude/scripts/search.ts --agent code-reviewer
bun ~/.claude/skills/find-claude/scripts/search.ts --limit 30 "auth"

Semantic search (--semantic) — uses episodic-memory embeddings for natural language similarity matching. Best for fuzzy/conceptual queries where you don't know the exact words used.

bun ~/.claude/skills/find-claude/scripts/search.ts --semantic "debugging timestamp issues in doltgres"
bun ~/.claude/skills/find-claude/scripts/search.ts --semantic "figma bridge port cleanup"

Flags can be combined: --skill eng:spec --branch feat/auth "credential" requires all flag conditions AND scores text terms.

How to construct the query:

  • Keyword mode (default): Pass the exact string you expect to find in session messages. The full string is matched as a substring — "figma hook" matches text containing that exact phrase. For broader matches, use shorter/simpler strings.
  • Semantic mode (--semantic): Pass a natural language description. The embedding model finds conceptually similar content even if the exact words differ.

Use flags (--skill, --pr, --branch, --file, --worktree, --repo, --title, --agent, --today) for structured filters. These apply to keyword mode only.

Rules:

  • For keyword: use the exact phrase you expect to appear in the session. Shorter = broader matches.
  • If the user mentions a PR, branch, skill, or file — use the flag, not a text term.
  • If exact keyword search misses, retry with --semantic for fuzzy matching.
User says Query
"find the figma bridge port cleanup hook conversation" "figma hook" or --semantic "figma bridge port cleanup hook"
"the one where we were speccing out openbolts credential delegation" --skill eng:spec "openbolt" or --semantic "openbolt credential delegation"
"conversations about PR 2212" --pr 2212
"what was I working on today?" --today
"where we were debugging doltgres timestamp issues" --skill eng:debug "doltgres timestamp"
"all conversations that touched manage-schema.ts" --file manage-schema.ts
"sessions named 'auth refactor'" --title "auth refactor"
"conversations using the code-reviewer agent" --agent code-reviewer

Retry loop — reformulate if results are weak:

If the first search returns no results or nothing that looks relevant (low scores, wrong topics):

  1. Shorten the query"figma hook port cleanup""figma hook""hook"
  2. Try --semantic — if exact string match misses, semantic search finds conceptually similar content
  3. Try synonyms"auth""authentication", "credential", "login"
  4. Switch from text to flags — if you know the topic involved a specific skill, branch, or file, try a flag-only search
  5. Try grep fallback (Step 3) — searches full JSONL content, catches things neither engine indexed

Do up to 3 reformulations before falling back to grep. Each retry should change strategy, not just repeat.

How scoring works:

  • Keyword score: Exact string match weighted by field — lastUserMessages (3x), firstUserMessages (2x), continuationSummaries (2x), structured fields (1x)
  • Semantic score: Cosine similarity from local MiniLM-L6 embeddings (384-dim) against every user/assistant exchange
  • Default limit: 20 results

The output is JSON with foundBy: ["keyword"] or foundBy: ["semantic"] indicating which engine found results. Read it and synthesize human-readable summaries per Step 4.

Dependency: Semantic search (--semantic) requires episodic-memory installed at ~/.claude/oss-repos/episodic-memory/ with npm link. Run episodic-memory sync to index new sessions.

Step 3: Grep fallback

If both keyword and semantic search return no matches (the keyword may be buried in the middle of a conversation, not captured in bookends, summaries, or embedding text):

grep -rl "KEYWORD" ~/.claude/projects/*/*.jsonl

For each hit, extract first + last user messages using head/tail and inline parsing to present context.

Compression does NOT delete messages from the JSONL file — it only affects the live context window. The full history is always on disk, so grep always searches complete content.

Step 4: Present results

For every match, synthesize a human-readable summary and explain why this session matches the query. Do NOT just dump raw field values — interpret them.

For each result, produce:

### {title if available, else one-line summary synthesized from first/last messages, skills, files, and PRs}

**Why this matches:** {1-2 sentences explaining why this session is relevant to the user's query — connect the search terms to what actually happened in the session}

**Started as:** {natural language paraphrase of firstUserMessages — not raw text dump}
**Left off at:** {natural language paraphrase of lastUserMessages — what state was the work in?}
**Context:** {launchDir} | {branches} | {messageCount} messages | {compactionCount} compactions
**Agent:** {agentSetting if set} | **Skills used:** {skills}
**Created at:** {startedAt — human-readable timestamp}
**Updated at:** {lastActiveAt — human-readable timestamp}
**PRs:** {prs} | **Files modified:** {count} | **Tags:** {tags if any}

```bash
cd {launchDir} && claude -r {id}

**How to write the summary line:** Read the firstUserMessages, lastUserMessages, skills, filesModified, prs, and branches together. Synthesize a short description of what the session was *about* — not what the user literally typed, but what the work was. Examples:
- "Speccing out credential delegation for openbolts using /spec"
- "Debugging doltgres timestamp issues in a worktree (PR #2212)"
- "Researching motion graphics tools and evaluating Remotion vs alternatives"
- "Creating blog cover options for agent-in-slack post using /graphics in Figma"

**How to write the "why this matches" line:** Connect the user's search query to the specific evidence in the session. Examples:
- User searched "openbolt spec" → "This session explored openbolts credential delegation and produced a SPEC.md — the lastUserMessages show the spec was in progress when the session ended."
- User searched "PR #2212" → "PR #2212 (inkeep/agents) was the primary focus — 24 gh pr commands were issued including diff review and body edits."

**Rules:**
- `launchDir` is the directory the user must `cd` into before resuming (Claude Code is project-scoped).
- Show at most 10 results unless the user asks for more.
- If multiple sessions are clearly about the same topic (same branch, overlapping PRs, sequential dates), group them and note the relationship: "These 3 sessions appear to be continuations of the same work" with the most recent one highlighted as the best resume target.
- If a session has `compactionCount > 0`, note this — it indicates a long/complex session where significant work happened.
- If `continuationSummaries` exist, use them to enrich your summary — they capture chronological recaps of pre-compaction work and are often the richest source of what the session accomplished.
- For resumption queries ("where was I on X?"), lead with the **Left off at** state and frame the summary around what remains to be done.

## Understanding the data

### Where sessions live
`~/.claude/projects/{project-path}/{session-id}.jsonl`

The project path encodes the working directory with all non-alphanumeric characters replaced by dashes (not just `/`):
- `~/agents` → `-Users-{username}-agents`
- `~/team-skills` → `-Users-{username}-team-skills`
- `~/InkeepDev/marketing-site-v2/marketing-site` → `-Users-{username}-InkeepDev-marketing-site-v2-marketing-site`
- `~/` → `-Users-{username}`
- Paths longer than 200 characters are truncated with a hash suffix for uniqueness.

### What's in each JSONL entry
Each line is a JSON object. Common types include: user/assistant messages, `system` (including `compact_boundary` subtype), `custom-title`, `ai-title`, `tag`, `agent-setting`, `pr-link`, `file-history-snapshot`, `attribution-snapshot`, `queue-operation`, `task-summary`, and others. Message entries carry metadata: `sessionId`, `cwd`, `gitBranch`, `timestamp`, `entrypoint`, `version`.

### What the indexer extracts per session

| Field | Source | Why it matters |
|---|---|---|
| `startedAt` | First timestamp in session | When the conversation was created |
| `lastActiveAt` | Last timestamp in session | When the conversation was last active |
| `title` | `custom-title` entries (user-set, wins) or `ai-title` entries (AI-generated) | Session name — searchable via `--title` |
| `agentSetting` | `agent-setting` entries | Which agent definition was used — searchable via `--agent` |
| `tags` | `tag` entries | Searchable session tags |
| `firstUserMessages` | First 3 user messages | What the conversation started as |
| `lastUserMessages` | Last 3 user messages | Where the conversation left off (handles topic drift) |
| `branches` | `gitBranch` field on every entry | Tracks branch changes mid-session |
| `worktrees` | `git worktree add` commands | Identifies worktree-based work |
| `prs` | `pr-link` entries + PR URLs in user messages + `gh pr` commands | Links sessions to PRs |
| `repos` | `pr-link` entries + PR URLs + `--repo` flags | Which repositories were touched |
| `skills` | `Skill` tool_use entries + auto-loaded skill detection | Which skills were invoked or loaded |
| `filesModified` | `Write`/`Edit` tool_use entries | Which files were changed |
| `toolCounts` | All tool_use entries | Session character (read-heavy, write-heavy, bash-heavy) |
| `compactionCount` | `system:compact_boundary` entries | Session length/complexity indicator |
| `continuationSummaries` | "continued from previous conversation" messages | Captures pre-compaction context (the "compressed middle") |

### Compression and the "needle in the middle" problem
Claude Code compresses context when approaching limits, but compression only affects the live context window — the full message history stays in the JSONL file on disk. The indexer captures both bookends (first + last messages) and continuation summaries (which chronologically recap pre-compaction content). Between these and the grep fallback, no content is unreachable.

### Navigating compacted conversations

When a session compacts, two things appear in the JSONL:

**1. A `compact_boundary` system entry** marking where compaction happened:
```json
{
  "type": "system",
  "subtype": "compact_boundary",
  "content": "Conversation compacted",
  "compactMetadata": { "trigger": "auto", "preTokens": 167469 },
  "timestamp": "2026-03-12T20:19:17.195Z"
}

2. A continuation summary as the next user message, starting with:

"This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation."

This summary is what the agent sees after compaction — it's the compressed version of everything before the boundary. The original messages are still in the JSONL above the boundary.

Shell recipes for compacted sessions:

# Count compactions in a session
grep -c 'compact_boundary' <session>.jsonl

# Find where each compaction happened (line numbers)
grep -n 'compact_boundary' <session>.jsonl

# Read the continuation summary (what the agent saw after compaction)
grep -A0 'continued from a previous conversation' <session>.jsonl | head -1 | python3 -c "
import sys, json
obj = json.loads(sys.stdin.read())
for c in obj.get('message',{}).get('content',[]):
    if isinstance(c, dict): print(c.get('text','')[:1000])
"

# Read pre-compaction messages (everything before the boundary — still on disk)
# Line N = compact_boundary → lines 1 to N-1 are pre-compaction
BOUNDARY=$(grep -n 'compact_boundary' <session>.jsonl | head -1 | cut -d: -f1)
head -$((BOUNDARY - 1)) <session>.jsonl | grep '"type":"user"' | tail -5

Key insight for debugging: After compaction, the agent loses tool results, loaded skill content, and detailed earlier exchanges. If a subprocess made a bad decision late in a session, check whether the information it needed was before a compact boundary — it may have been working from the summary, not the original context.


Session IDs and how they flow

Where session IDs come from

Every Claude Code session gets a UUID (e.g., e76656ec-750d-4480-9e1c-bf57f8f9a73b) generated at session start. The ID is:

  • The filename (without .jsonl) in ~/.claude/projects/<project>/
  • Embedded in every JSONL entry's sessionId field
  • Returned in --output-format json output as .session_id
  • Used by --resume <id> and --resume <id> --fork-session to continue/fork sessions

How to get a session ID

Context How to get it
Interactive session Check ~/.claude/projects/<project>/ — most recently modified .jsonl file
--output-format json subprocess Parse jq -r '.session_id' from the JSON output
Named session (--name) grep -l '"customTitle":"_nest:my-label"' ~/.claude/projects/<project>/*.jsonl
Background task Read the task output file, parse session_id from JSON
Ship review run Check tmp/ship/local-review-runs/<run-id>/claude-output.log — parse session_id from JSON

Worktree sessions

Worktrees get their own project key. If the main checkout is at /Users/me/myrepo and the worktree is at /Users/me/myrepo-feature, sessions go to:

  • Main: ~/.claude/projects/-Users-me-myrepo/
  • Worktree: ~/.claude/projects/-Users-me-myrepo-feature/

A claude -p subprocess's project key depends on its CWD at launch time, NOT the parent session's project key.


Forensic investigation — debugging what a session did

When something goes wrong in a subprocess (review loop, implementation iteration, nested agent), use these patterns to trace what happened.

Find a session by ID

# From a known session ID (e.g., from --output-format json or run metadata)
SESSION_ID="e76656ec-750d-4480-9e1c-bf57f8f9a73b"
find ~/.claude/projects -name "${SESSION_ID}.jsonl" 2>/dev/null

Check what CWD a subprocess used

# The CWD is on every JSONL entry — check the first one
head -1 ~/.claude/projects/<project>/<session-id>.jsonl | jq -r '.cwd'

# Or check the assistant entries (which record CWD at response time)
grep '"type":"assistant"' <session>.jsonl | head -1 | jq -r '.cwd'

Find what files a session wrote

# All Write tool calls with file paths
grep -o '"name":"Write".*?"file_path":"[^"]*"' <session>.jsonl | grep -o '"file_path":"[^"]*"'

# All Edit tool calls
grep -o '"name":"Edit".*?"file_path":"[^"]*"' <session>.jsonl | grep -o '"file_path":"[^"]*"'

Find what a session wrote to a specific file

grep '"file_path":"/path/to/specific/file"' <session>.jsonl | head -3

Check which tools a session called

# Count tool calls by name
grep -o '"name":"[^"]*"' <session>.jsonl | sort | uniq -c | sort -rn | head -20

Find error messages in a session

grep -i '"isError":true\|"error"\|"Error"' <session>.jsonl | head -10

Check the session's git branch

grep -o '"gitBranch":"[^"]*"' <session>.jsonl | sort -u

View the session's final output (for --output-format json subprocesses)

# The last line of a JSON-output session contains the result
tail -1 <session>.jsonl | jq '{result: .result, session_id: .session_id, cost: .total_cost_usd, duration_ms: .duration_ms}'

List recent sessions sorted by time

# Most recently modified sessions across all projects
ls -lt ~/.claude/projects/*/*.jsonl | head -20

# Most recent for a specific project
ls -lt ~/.claude/projects/-Users-me-myrepo/*.jsonl | head -10

Find named sessions (from --name flag)

# Find all sessions named with _nest: prefix
grep -rl '"customTitle":"_nest:' ~/.claude/projects/ 2>/dev/null

# Find a specific named session
grep -rl '"customTitle":"_nest:pr-review"' ~/.claude/projects/<project>/ 2>/dev/null

Find child sessions spawned by a parent

When /ship or /nest-claude spawns subprocesses, child sessions are separate .jsonl files. To find them:

# Children are typically in the same project dir, created around the same time
# List sessions created in the last hour for this project
find ~/.claude/projects/<project> -name "*.jsonl" -mmin -60 | sort

# Children spawned by /ship often have _nest: names
grep -l '"customTitle":"_nest:' ~/.claude/projects/<project>/*.jsonl 2>/dev/null

# Or find by the branch (children inherit the git branch)
grep -l '"gitBranch":"feat/my-feature"' ~/.claude/projects/<project>/*.jsonl 2>/dev/null

Trace the path resolution chain (for debugging worktree/CWD issues)

When a subprocess writes to the wrong path, trace where it thought it was:

SESSION_FILE="~/.claude/projects/<project>/<session-id>.jsonl"

# 1. What CWD did the subprocess use?
head -1 "$SESSION_FILE" | jq -r '.cwd'

# 2. What absolute paths did it write to?
grep -o '"file_path":"/[^"]*"' "$SESSION_FILE" | sort -u

# 3. What branch was it on?
grep -o '"gitBranch":"[^"]*"' "$SESSION_FILE" | head -1

# 4. Was it a worktree? (compare CWD to main checkout)
# If CWD differs from the main checkout path, it was in a worktree

Ship review run artifacts

When /ship review fails, the run artifacts contain the subprocess output:

# List all review runs
ls tmp/ship/local-review-runs/

# The run dir contains:
# - claude-output.log   — raw JSON output from claude -p (session_id, result, cost)
# - run-metadata.txt    — exit codes, timestamps, paths, branch
# - full.diff           — the diff that was reviewed
# - pr-context.md       — the context skill output

# Extract session ID from a run
jq -r '.session_id' tmp/ship/local-review-runs/<run-id>/claude-output.log | tail -1

# Check exit codes
grep 'claude_.*exit_code' tmp/ship/local-review-runs/<run-id>/run-metadata.txt
Related skills

More from inkeep/team-skills

Installs
3
GitHub Stars
10
First Seen
Mar 20, 2026