Original Reddit post

Claude Code saves everything — plans, session transcripts, auto-memory — but gives you no way to search it. After a few weeks of heavy use, I had 60 plan files with auto-generated names like whimsical-mixing-shore.md and thousands of entries in session history. Good luck finding that authentication architecture discussion from last Tuesday. I built /search-memory — a Claude Code skill that searches across both plan files and session history. ~140 lines of bash, grep-based, no dependencies beyond Python 3 (for JSONL parsing). Background If you saw my previous post on replacing the Explore agent , you know I went through a phase of building custom infrastructure for Claude Code — pre-computed structural indexes, a custom Explore agent, SessionStart hooks generating project maps. It worked, but the maintenance overhead wasn’t worth the gains. I scrapped all of it in favor of leaning into Claude Code’s built-in features: auto-memory, MEMORY.md , and the self-improvement loop from the wrap-up skill. That shift left one gap. Claude Code accumulates two valuable data stores over time: Plans ( ~/.claude/plans/*.md ) — Architecture decisions, implementation strategies, research notes. Claude auto-generates these during plan mode with whimsical filenames you’ll never remember. Session history ( ~/.claude/history.jsonl ) — Every session’s first message, timestamped and tagged with a session ID. Both are just files on disk. Both are searchable with basic tools. Neither has a built-in search UI. The Skill Two files: Skill definition ( ~/.claude/skills/search-memory/SKILL.md )

name: search-memory
description: Search across saved plans and session history
Search Memory
Search across saved plans (
~/.claude/plans/
) and session history (
~/.claude/history.jsonl
) to find past work, decisions, and conversations.
Usage
User invokes with:
/search-memory <query>
or
/search-memory <query> --plans
or
/search-memory <query> --sessions
Steps
Run the search script:
~/.claude/scripts/search-memory.sh "<query>" [--plans|--sessions|--all]
Default scope is
--all
(searches both plans and sessions).
Present results clearly:
Plans:
Show filename, title, modification date, and matching context lines. Include the full file path so the user can ask to read a specific plan.
Sessions:
Show date, first message text, and session ID. Note that
/resume <sessionId>
can reopen a session.
If the user wants to dig deeper into a specific plan,
Read
the file and summarize its contents.
If no results found, suggest alternative search terms. ```
The skill file tells Claude
how to present the results
— that's the part that makes this feel like a real feature instead of raw grep output. Plans get file paths you can ask Claude to read. Sessions get IDs you can pass to
/resume
to reopen them.
Search script (
~/.claude/scripts/search-memory.sh
)
```bash
!/usr/bin/env bash
search-memory.sh — Search Claude plans and session history
Usage: search-memory.sh <query> [--plans|--sessions|--all]
set -euo pipefail
PLANS_DIR="$HOME/.claude/plans" HISTORY_FILE="$HOME/.claude/history.jsonl"
usage() { echo "Usage: search-memory.sh <query> [--plans|--sessions|--all]" echo " --plans Search only plan files" echo " --sessions Search only session history" echo " --all Search both (default)" exit 1 }
Parse args
QUERY="" SCOPE="all"
while [[ $# -gt 0 ]]; do case "$1" in --plans) SCOPE="plans"; shift ;; --sessions) SCOPE="sessions"; shift ;; --all) SCOPE="all"; shift ;; -h|--help) usage ;; -*) echo "Unknown option: $1"; usage ;; *) if [[ -z "$QUERY" ]]; then QUERY="$1" else echo "Error: multiple query arguments" usage fi shift ;; esac done
if [[ -z "$QUERY" ]]; then echo "Error: query is required" usage fi
── Plan search ──
search_plans() { if [[ ! -d "$PLANS_DIR" ]]; then echo " (no plans directory found)" return fi
local matches matches=$(grep -ril "$QUERY" "$PLANS_DIR"/*.md 2>/dev/null || true) if [[ -z "$matches" ]]; then echo " (no matches)" return fi echo "$matches" | while read -r file; do local mtime title preview mtime=$(stat -f "%Sm" -t "%Y-%m-%d" "$file" 2>/dev/null \ || echo "unknown") title=$(head -1 "$file" | sed 's/^#\s*//') preview=$(grep -i "$QUERY" "$file" | head -2 | sed 's/^/ /') echo " [$mtime] $(basename "$file")" echo " $title" if [[ -n "$preview" ]]; then echo "$preview" fi echo "" done | sort -t'[' -k2 -r
}
── Session search ──
search_sessions() { if [[ ! -f "$HISTORY_FILE" ]]; then echo " (no history file found)" return fi
# Validate JSONL format hasn't changed local first_line first_line=$(head -1 "$HISTORY_FILE") if ! echo "$first_line" | python3 -c \ "import sys,json; d=json.load(sys.stdin); \ assert 'display' in d and 'timestamp' in d \ and 'sessionId' in d" 2>/dev/null; then echo " Error: history.jsonl format has changed" return fi grep -i "$QUERY" "$HISTORY_FILE" 2>/dev/null | \ python3 -c "
import sys, json from datetime import datetime
seen = {} for line in sys.stdin: line = line.strip() if not line: continue try: entry = json.loads(line) sid = entry.get('sessionId', '') display = entry.get('display', '').strip() ts = entry.get('timestamp', 0) if sid and sid not in seen: seen[sid] = (ts, display, sid) except (json.JSONDecodeError, KeyError): continue
for ts, display, sid in sorted( seen.values(), key=lambda x: x[0], reverse=True ): date = datetime.fromtimestamp(ts / 1000).strftime('%Y-%m-%d %H:%M') if len(display) > 120: display = display[:117] + '...' print(f' [{date}] {display}') print(f' Session: {sid}') print() " 2>/dev/null || echo " Error: failed to parse history.jsonl" }
── Run search ──
echo "Searching for: \"$QUERY\"" echo ""
if [[ "$SCOPE" == "plans" || "$SCOPE" == "all" ]]; then echo "=== Plans ===" search_plans fi
if [[ "$SCOPE" == "sessions" || "$SCOPE" == "all" ]]; then echo "=== Sessions ===" search_sessions fi ```
What it does
Plan search:
grep -ril
through
~/.claude/plans/*.md
. For each match, extracts the title (first line), modification date, and 2 lines of matching context. Sorted by date, most recent first.
Session search:
grep -i
through
~/.claude/history.jsonl
, then pipes to Python for JSONL parsing. Deduplicates by session ID (history can have multiple entries per session), truncates long display text, sorts by timestamp. The format validation on the first line is a safety check — if Anthropic changes the JSONL schema, you get a clear error instead of garbage output.
Usage
/search-memory quiz app /search-memory authentication --plans /search-memory deploy --sessions
Claude runs the script, then presents the results in a readable format. For plans, it shows file paths you can ask it to read. For sessions, it shows session IDs you can pass to
/resume
.
Example output for
/search-memory deploy --sessions
:
``` === Sessions === [2026-02-17 05:17] Isn't there a deploy skill in the quiz and bot projects already? Session: a1b2c3d4-e5f6-7890-abcd-ef1234567890
[2026-02-15 06:51] Let's set up the CI pipeline for staging deployments... Session: f9e8d7c6-b5a4-3210-fedc-ba9876543210
[2026-02-13 17:48] I want to automate the deploy process so it runs tests first... Session: 1a2b3c4d-5e6f-7890-1234-567890abcdef ```
See a session you want to revisit?
/resume a1b2c3d4-e5f6-7890-abcd-ef1234567890
drops you right back in with full context.
Setup
Create the script at
~/.claude/scripts/search-memory.sh
and
chmod +x
it
Create the skill at
~/.claude/skills/search-memory/SKILL.md
That's it. No hooks, no build step, no indexing
The skill is available immediately in your next session. Type
/search-memory
and Claude knows what to do.
Design choices
Grep, not a database.
60 plan files and a few thousand JSONL lines search instantly with grep. No need for SQLite or full-text indexing at this scale. If you somehow accumulate 100K+ sessions, swap grep for ripgrep.
Python only for JSONL.
Bash can't reliably parse JSON. The Python block is stdlib-only — no pip installs. It handles deduplication, timestamp formatting, and sorting in one pass.
Format validation.
The session search checks the first line of
history.jsonl
for expected fields before processing. Claude Code is pre-1.0 — the internal format could change anytime. Better to fail with a clear error than silently return wrong results.
Skill file does the UX work.
The bash script outputs raw text. The skill file tells Claude to present plan results with full file paths (so you can say "read that plan") and session results with IDs (so you can
/resume
). The intelligence layer is in the prompt, not the script.
Two files, ~140 lines of bash, zero dependencies. Works today, degrades gracefully if the underlying format changes. Happy to answer questions or help you adapt this to your setup.
submitted by
/u/jonathanmalkin

Originally posted by u/jonathanmalkin on r/ClaudeCode