docs: add cc-sessions discover + GitHub repo (v1.0.0)

- New subsection "Session Pattern Discovery" in §2.x (Session Management):
  n-gram mode, --llm mode via claude --print, example output, 20% rule framework
- Cross-reference added after the 20% rule callout in §5.1 Skills
- examples/scripts/cc-sessions.py synced: 498 → 1225 lines (full discover subcommand)
- examples/scripts/README.md: discover examples + curl install + GitHub link
- machine-readable/reference.yaml: cc_sessions_github + cc_sessions_discover entries
- GitHub repo created: https://github.com/FlorianBruniaux/cc-sessions (v1.0.0 released)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Florian BRUNIAUX 2026-03-13 15:28:31 +01:00
parent 1728b6de39
commit 13efb5a774
5 changed files with 1029 additions and 9 deletions

View file

@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- **`cc-sessions discover` documentation** — New subsection "Session Pattern Discovery" in §2.x (Session Management) covering the `discover` subcommand: n-gram mode (local, free, ~3s for 12 projects) vs `--llm` mode (semantic analysis via `claude --print`). Includes example output, the 20% rule decision framework (CLAUDE.md rule / skill / command categorization), and install instructions. Cross-reference added after the 20% rule callout in §5.1.
- **`examples/scripts/cc-sessions.py` synced** — Updated from 498-line stale copy to the full 1225-line version from `~/bin/cc-sessions`. Includes the complete `discover` subcommand (n-gram analysis + `--llm` mode), incremental discover cache, Jaccard deduplication, and all filtering logic. GitHub source header added.
- **`examples/scripts/README.md`** — Updated cc-sessions entry: added `discover` subcommand examples (n-gram and `--llm`), GitHub repo link ([FlorianBruniaux/cc-sessions](https://github.com/FlorianBruniaux/cc-sessions)), and curl install instructions.
- **`machine-readable/reference.yaml`** — Added `cc_sessions_github` and `cc_sessions_discover` entries alongside the updated `cc_sessions_script` comment.
- **GitHub repo created**: [FlorianBruniaux/cc-sessions](https://github.com/FlorianBruniaux/cc-sessions) — v1.0.0 release tagged and published.
## [3.34.11] - 2026-03-13
### Updated
- **`guide/ultimate-guide.md`** (§ Cost Optimization → Strategy 6): Added `#### How Claude Code Handles Caching Automatically` subsection (~75 lines) covering the mechanics that were previously a single undocumented footnote. New content: (1) **Cache prefix hierarchy**`tools → system → messages` ordering and why the first two layers almost always hit; (2) **20-block lookback** — the long-session cache degradation trap and why `/compact` restores hit rates; (3) **Minimum token thresholds by model** — eligibility table for Opus/Sonnet/Haiku families (1,0244,096 tokens), correcting the previously circulating false "32,000 token maximum" claim; (4) **Tool result size and cache economics** — why compact tool outputs reduce both cache write and read costs proportionally across the entire session; (5) **Monitoring in custom pipelines**`cache_creation_input_tokens` / `cache_read_input_tokens` response fields, hit rate calculation formula, and why no dedicated CC cache monitoring tool currently exists; (6) **Practical rules** — CLAUDE.md stability, pre-emptive `/compact` timing, avoiding dynamic content in stable sections.
## [3.34.10] - 2026-03-13
### Added

View file

@ -271,9 +271,11 @@ Search across all Claude Code session histories.
## Session Manager (Advanced)
Advanced CLI for session search, browse & resume with incremental indexing.
Advanced CLI for session search, browse, resume & pattern discovery with incremental indexing.
**vs session-search.sh**: Faster search (~200ms vs ~400ms), partial ID resume, branch filter, worktree support, incremental JSONL index.
**vs session-search.sh**: Faster search (~200ms vs ~400ms), partial ID resume, branch filter, worktree support, incremental JSONL index, and `discover` subcommand for automated config optimization.
**GitHub**: [FlorianBruniaux/cc-sessions](https://github.com/FlorianBruniaux/cc-sessions)
```bash
# Search in current project
@ -293,11 +295,26 @@ cc-sessions resume 8d472d
# JSON output for scripting
cc-sessions --json search "prisma" | jq -r '.[].id'
# Discover recurring patterns (n-gram, local, free)
cc-sessions --all discover
# Discover with semantic analysis via claude --print
cc-sessions --all discover --llm
# JSON output for scripting
cc-sessions --all discover --json | jq '.[] | select(.category == "skill")'
```
**Installation**: `cp cc-sessions.py ~/bin/cc-sessions && chmod +x ~/bin/cc-sessions`
**Install from GitHub**:
```bash
curl -sL https://raw.githubusercontent.com/FlorianBruniaux/cc-sessions/main/cc-sessions \
-o ~/.local/bin/cc-sessions && chmod +x ~/.local/bin/cc-sessions
```
> [Gist source](https://gist.github.com/FlorianBruniaux/992d4d1107592d9e98ca9d89838871c6)
**Or copy locally**: `cp cc-sessions.py ~/bin/cc-sessions && chmod +x ~/bin/cc-sessions`
> [GitHub repo](https://github.com/FlorianBruniaux/cc-sessions) · [Gist](https://gist.github.com/FlorianBruniaux/992d4d1107592d9e98ca9d89838871c6)
---

View file

@ -1,4 +1,5 @@
#!/usr/bin/env python3
# Source: https://github.com/FlorianBruniaux/cc-sessions
"""
cc-sessions Fast CLI to search, browse & resume Claude Code session history
@ -10,6 +11,7 @@ This tool indexes those sessions for fast search and provides a clean CLI interf
- Filter by date, branch, or project
- View recent sessions
- Resume past sessions with partial ID matching
- Discover recurring patterns to extract as skills, commands, or CLAUDE.md rules
FEATURES
--------
@ -19,6 +21,7 @@ FEATURES
- 🎯 Partial ID matching: 'cc-sessions resume 8d472d' finds full session ID
- 🌳 Worktree support: includes git worktree sessions automatically
- 📊 JSON output: pipe to jq/fzf for advanced workflows
- 🔭 Pattern discovery: analyze sessions to suggest skills/commands/rules
- 🐍 Zero dependencies: Python stdlib only (json, argparse, pathlib)
USAGE
@ -44,6 +47,12 @@ cc-sessions resume 8d472d2c
# Force rebuild index
cc-sessions reindex
# Discover patterns (all projects, last 90 days)
cc-sessions discover
# Discover with custom filters
cc-sessions --all discover --since 60d --min-count 2 --top 15
# JSON output for composition
cc-sessions --json search "prisma" | jq -r '.[].id'
@ -54,6 +63,10 @@ INSTALLATION
3. Run: cc-sessions recent 5
(First run builds index ~10s for 1500 sessions, then <200ms)
Or install from GitHub:
curl -sL https://raw.githubusercontent.com/FlorianBruniaux/cc-sessions/main/cc-sessions \
-o ~/.local/bin/cc-sessions && chmod +x ~/.local/bin/cc-sessions
INDEX ARCHITECTURE
------------------
- Location: ~/.claude/sessions-index.jsonl (~360KB for 1300 sessions)
@ -109,19 +122,28 @@ cc-sessions positioning: Unix-style CLI, fast search, powerful filters, no depen
AUTHOR
------
Created for terminal power users who prefer CLI over GUI.
GitHub: https://github.com/FlorianBruniaux/cc-sessions
Gist: https://gist.github.com/FlorianBruniaux/992d4d1107592d9e98ca9d89838871c6
"""
import argparse
import json
import os
import re
import sys
import urllib.error
import urllib.request
from collections import Counter, defaultdict
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Tuple
CLAUDE_DIR = Path.home() / ".claude"
INDEX_PATH = CLAUDE_DIR / "sessions-index.jsonl"
DISCOVER_CACHE_PATH = CLAUDE_DIR / "discover-cache.jsonl"
LLM_BATCH_SIZE = 60
LLM_DEFAULT_MODEL = '' # empty = use claude CLI default model
def parse_duration(duration_str: str) -> datetime:
@ -204,6 +226,49 @@ def get_first_user_message(filepath: Path) -> Optional[str]:
return None
def extract_all_user_messages(filepath: Path) -> List[str]:
"""Extract all significant user messages from a session JSONL file."""
messages = []
try:
with open(filepath, 'r') as f:
for line in f:
if not line.strip():
continue
try:
entry = json.loads(line)
if entry.get('type') != 'user':
continue
content = entry.get('message', {}).get('content', '')
if not isinstance(content, str):
continue
if content.startswith('<'):
continue
stripped = content.strip()
# Skip very short messages (likely acknowledgements)
if len(stripped) < 10:
continue
# Skip system-injected messages (compact summaries, reminders)
# These are injected as plain-text user messages but aren't real user input
if len(stripped) > 800:
continue
if _is_system_injection(stripped):
continue
messages.append(stripped)
except json.JSONDecodeError:
continue
except Exception:
pass
return messages
def parse_session(filepath: Path) -> Optional[Dict]:
"""Extract session metadata."""
session_id = filepath.stem
@ -438,6 +503,631 @@ def cmd_reindex():
print(f"Indexed {len(index)} sessions", file=sys.stderr)
# ─── DISCOVER subcommand ──────────────────────────────────────────────────────
# Boilerplate phrases that identify system-injected messages (compact summaries,
# system-reminder injections, plan mode prompts, task tool notifications...).
# These appear as plain-text user messages in JSONL but are not real user input.
_SYSTEM_INJECTION_MARKERS = (
'this session is being continued',
'read the full transcript',
'context summary below covers',
'exact snippets error messages content',
'exiting plan mode',
'task tools haven',
'teamcreate tool team parallelize',
'florianbruniaux/.claude/projects', # path fragments in compact prompts
'florianbruniaux/sites/', # project path fragments
'-users-florianbruniaux-', # encoded path in compact messages
)
def _is_system_injection(text: str) -> bool:
"""Return True if this looks like a Claude Code system message, not real user input."""
lower = text.lower()
return any(marker in lower for marker in _SYSTEM_INJECTION_MARKERS)
# Stop words to exclude from n-gram analysis
_STOP_WORDS = frozenset({
'a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
'of', 'with', 'by', 'from', 'is', 'it', 'its', 'be', 'as', 'was',
'are', 'were', 'been', 'have', 'has', 'had', 'do', 'does', 'did',
'will', 'would', 'could', 'should', 'may', 'might', 'can', 'shall',
'this', 'that', 'these', 'those', 'i', 'you', 'we', 'they', 'he',
'she', 'my', 'your', 'our', 'their', 'his', 'her', 'me', 'us', 'them',
'so', 'if', 'then', 'than', 'when', 'what', 'how', 'why', 'where',
'who', 'which', 'not', 'no', 'also', 'just', 'now', 'up', 'out',
'about', 'into', 'after', 'before', 'all', 'any', 'some', 'more',
'new', 'add', 'use', 'make', 'get', 'go', 'run', 'see', 'here',
'there', 'need', 'want', 'please', 'ok', 'okay', 'yes', 'yeah',
'let', 'can', 'help', 'look', 'check', 'same', 'like', 'very',
'much', 'only', 'other', 'also', 'each', 'file', 'code', 'create',
'update', 'change', 'think', 'know', 'give', 'take', 'put', 'keep',
})
def normalize_text(text: str) -> List[str]:
"""Lowercase, strip punctuation, tokenize, remove stop words."""
text = text.lower()
# Replace punctuation/special chars with spaces (keep alphanumeric and hyphens)
text = re.sub(r'[^a-z0-9\s\-]', ' ', text)
# Collapse whitespace
tokens = text.split()
# Filter stop words and very short tokens
return [t for t in tokens if t not in _STOP_WORDS and len(t) > 2]
def extract_ngrams(tokens: List[str], n: int) -> List[Tuple[str, ...]]:
"""Extract n-grams from a token list."""
return [tuple(tokens[i:i+n]) for i in range(len(tokens) - n + 1)]
def token_overlap(tokens_a: List[str], tokens_b: List[str]) -> float:
"""Jaccard similarity between two token sets."""
if not tokens_a or not tokens_b:
return 0.0
set_a, set_b = set(tokens_a), set(tokens_b)
return len(set_a & set_b) / len(set_a | set_b)
def load_discover_cache() -> Dict[str, Dict]:
"""Load the discover cache (session_id -> {mtime, messages[]})."""
if not DISCOVER_CACHE_PATH.exists():
return {}
cache = {}
try:
with open(DISCOVER_CACHE_PATH, 'r') as f:
for line in f:
if not line.strip():
continue
entry = json.loads(line)
cache[entry['id']] = entry
except Exception:
return {}
return cache
def save_discover_cache(cache: Dict[str, Dict]):
"""Persist the discover cache to disk."""
CLAUDE_DIR.mkdir(exist_ok=True)
with open(DISCOVER_CACHE_PATH, 'w') as f:
for entry in cache.values():
f.write(json.dumps(entry) + '\n')
def collect_sessions_data(
project_dirs: List[Path],
since_dt: Optional[datetime],
) -> List[Dict]:
"""
Collect {session_id, project, mtime, messages[]} for all sessions.
Uses mtime-based cache to avoid re-reading unchanged files.
Returns one dict per session that has at least one user message.
"""
cache = load_discover_cache()
updated_cache = {}
sessions_data = []
for project_dir in project_dirs:
project_name = project_dir.name
jsonl_files = list(project_dir.glob("*.jsonl"))
for filepath in jsonl_files:
session_id = filepath.stem
# Skip subagent sessions
if session_id.startswith('agent-'):
continue
try:
file_mtime = filepath.stat().st_mtime
except OSError:
continue
# Apply date filter: skip if file is older than since_dt
if since_dt:
file_dt = datetime.fromtimestamp(file_mtime)
if file_dt < since_dt:
continue
# Cache hit: file unchanged since last analysis
if session_id in cache and cache[session_id].get('mtime', 0) >= file_mtime:
entry = cache[session_id]
if entry.get('messages'):
sessions_data.append({
'session_id': session_id,
'project': project_name,
'mtime': file_mtime,
'messages': entry['messages'],
})
updated_cache[session_id] = entry
continue
# Cache miss: parse file
messages = extract_all_user_messages(filepath)
cache_entry = {
'id': session_id,
'mtime': file_mtime,
'messages': messages,
}
updated_cache[session_id] = cache_entry
if messages:
sessions_data.append({
'session_id': session_id,
'project': project_name,
'mtime': file_mtime,
'messages': messages,
})
save_discover_cache(updated_cache)
return sessions_data
def discover_patterns(
sessions_data: List[Dict],
min_count: int = 3,
top: int = 20,
) -> List[Dict]:
"""
Analyze sessions data and return pattern suggestions.
Each suggestion has:
- pattern: human-readable phrase
- count: number of occurrences
- session_count: number of distinct sessions
- project_count: number of distinct projects
- cross_project: bool
- category: 'CLAUDE.md rule' | 'skill' | 'command'
- score: float (frequency × cross-project bonus)
- example_sessions: list of up to 2 session_ids
"""
total_sessions = len(sessions_data)
if total_sessions == 0:
return []
# ── Step 1: Build per-session token lists and n-gram index ────────────────
# ngram_index: ngram_tuple -> list of {session_id, project, original_message}
ngram_index: Dict[Tuple, List[Dict]] = defaultdict(list)
for sd in sessions_data:
session_id = sd['session_id']
project = sd['project']
for msg in sd['messages']:
tokens = normalize_text(msg)
if len(tokens) < 3:
continue
for n in range(3, 7): # 3-6 word n-grams
for ngram in extract_ngrams(tokens, n):
# Filter n-grams that are all stop words (shouldn't happen after normalize)
if all(t in _STOP_WORDS for t in ngram):
continue
ngram_index[ngram].append({
'session_id': session_id,
'project': project,
'msg': msg[:80],
})
# ── Step 2: Filter n-grams below min_count ────────────────────────────────
frequent_ngrams = {
ng: occurrences
for ng, occurrences in ngram_index.items()
if len(occurrences) >= min_count
}
# ── Step 3: Deduplicate — prefer longer n-gram if it subsumes shorter ─────
# Sort by length desc, then count desc
sorted_ngrams = sorted(
frequent_ngrams.items(),
key=lambda x: (len(x[0]), len(x[1])),
reverse=True,
)
kept_ngrams: List[Tuple[Tuple, List[Dict]]] = []
subsumed: set = set()
for ngram, occurrences in sorted_ngrams:
if ngram in subsumed:
continue
kept_ngrams.append((ngram, occurrences))
# Mark all sub-ngrams of this ngram as subsumed
for n in range(3, len(ngram)):
for i in range(len(ngram) - n + 1):
sub = ngram[i:i+n]
subsumed.add(sub)
# ── Step 4: Similarity clustering — merge near-duplicate phrases ──────────
# Group kept_ngrams by token overlap > 60%
clusters: List[List[int]] = []
assigned = set()
for i, (ng_i, _) in enumerate(kept_ngrams):
if i in assigned:
continue
cluster = [i]
for j, (ng_j, _) in enumerate(kept_ngrams):
if j <= i or j in assigned:
continue
overlap = token_overlap(list(ng_i), list(ng_j))
if overlap > 0.6:
cluster.append(j)
assigned.add(j)
clusters.append(cluster)
assigned.add(i)
# ── Step 5: Build suggestion per cluster (representative = highest count) ──
suggestions = []
for cluster in clusters:
# Pick representative: longest ngram with most occurrences
best_idx = max(cluster, key=lambda i: (len(kept_ngrams[i][0]), len(kept_ngrams[i][1])))
ngram, occurrences = kept_ngrams[best_idx]
# Aggregate across cluster members
all_occurrences = []
for idx in cluster:
all_occurrences.extend(kept_ngrams[idx][1])
distinct_sessions = list({o['session_id'] for o in all_occurrences})
distinct_projects = list({o['project'] for o in all_occurrences})
count = len(all_occurrences)
session_count = len(distinct_sessions)
project_count = len(distinct_projects)
cross_project = project_count >= 2
if session_count < min_count:
continue
session_pct = session_count / total_sessions
# Categorize
if session_pct > 0.20:
category = 'CLAUDE.md rule'
elif session_pct >= 0.05:
category = 'skill'
else:
category = 'command'
# Score: base = session_pct, bonus × 1.5 if cross-project
score = session_pct * (1.5 if cross_project else 1.0)
phrase = ' '.join(ngram)
suggestions.append({
'pattern': phrase,
'count': count,
'session_count': session_count,
'project_count': project_count,
'cross_project': cross_project,
'category': category,
'score': round(score, 4),
'example_sessions': distinct_sessions[:2],
})
# ── Step 6: Sort by score desc, truncate ──────────────────────────────────
suggestions.sort(key=lambda x: x['score'], reverse=True)
return suggestions[:top]
def cmd_discover(
project_dirs: List[Path],
since: str = '90d',
min_count: int = 3,
top: int = 20,
json_output: bool = False,
):
"""Analyze sessions and surface patterns worth extracting as skills/commands/rules."""
since_dt = parse_duration(since)
print(f"Scanning sessions since {since_dt.strftime('%Y-%m-%d')}...", file=sys.stderr)
sessions_data = collect_sessions_data(project_dirs, since_dt)
if not sessions_data:
print("No sessions found in the given time range.", file=sys.stderr)
return
print(f"Analyzing {len(sessions_data)} sessions across "
f"{len({sd['project'] for sd in sessions_data})} project(s)...", file=sys.stderr)
suggestions = discover_patterns(sessions_data, min_count=min_count, top=top)
if not suggestions:
print("No recurring patterns found (try --min-count 2 or --since 180d).", file=sys.stderr)
return
if json_output:
print(json.dumps(suggestions, indent=2))
return
# ── Human-readable output ─────────────────────────────────────────────────
by_category: Dict[str, List[Dict]] = defaultdict(list)
for s in suggestions:
by_category[s['category']].append(s)
category_order = ['CLAUDE.md rule', 'skill', 'command']
category_icons = {
'CLAUDE.md rule': '📋',
'skill': '🧩',
'command': '',
}
total_sessions = len(sessions_data)
total_projects = len({sd['project'] for sd in sessions_data})
print()
print(f" cc-sessions discover — {total_sessions} sessions · {total_projects} project(s) · since {since}")
print()
for cat in category_order:
items = by_category.get(cat, [])
if not items:
continue
icon = category_icons[cat]
print(f" {icon} {cat.upper()}")
print(f" {'' * 60}")
for item in items:
tag = ' [cross-project]' if item['cross_project'] else ''
pct = item['session_count'] / total_sessions * 100
print(
f" {item['pattern']}"
f"{tag}"
)
print(
f" {item['session_count']} sessions ({pct:.0f}%) · "
f"{item['count']} occurrences · "
f"score {item['score']:.3f}"
)
for ex in item['example_sessions']:
print(f"{ex[:36]}")
print()
print()
print(f" Run with --json to pipe to jq for further processing.")
print()
# ─── DISCOVER --llm subcommand ────────────────────────────────────────────────
def deduplicate_messages_for_llm(sessions_data: List[Dict], max_messages: int = 300) -> List[Dict]:
"""
Deduplicate semantically similar messages using Jaccard similarity.
Returns list of {text, count, projects} sorted by frequency desc.
"""
all_msgs = []
for sd in sessions_data:
for msg in sd.get('messages', []):
all_msgs.append({
'text': msg[:500],
'project': sd['project'],
'tokens': normalize_text(msg),
})
if not all_msgs:
return []
clusters: List[List[int]] = []
assigned = set()
for i, m in enumerate(all_msgs):
if i in assigned:
continue
cluster = [i]
# Limit comparison window for performance on large sets
window_end = min(i + 300, len(all_msgs))
for j in range(i + 1, window_end):
if j in assigned:
continue
if token_overlap(m['tokens'], all_msgs[j]['tokens']) > 0.65:
cluster.append(j)
assigned.add(j)
clusters.append(cluster)
assigned.add(i)
deduped = []
for cluster in clusters:
representative = all_msgs[cluster[0]]
projects = list({all_msgs[i]['project'] for i in cluster})
deduped.append({
'text': representative['text'],
'count': len(cluster),
'projects': projects,
})
deduped.sort(key=lambda x: x['count'], reverse=True)
return deduped[:max_messages]
def build_analysis_prompt(messages: List[Dict]) -> str:
lines = []
for i, m in enumerate(messages, 1):
count_info = f" (x{m['count']})" if m['count'] > 1 else ""
cross = " [multi-project]" if len(m['projects']) > 1 else ""
text = m['text'][:200].replace('\n', ' ')
lines.append(f"{i}. {text}{count_info}{cross}")
messages_block = '\n'.join(lines)
return f"""You are analyzing a developer's Claude Code session history to find recurring patterns worth extracting as reusable configurations.
Below are user messages (deduplicated). Numbers in parentheses show how many times a similar message appeared. [multi-project] means it appeared across different codebases.
MESSAGES:
{messages_block}
Identify recurring patterns and suggest what to extract. For each suggestion, choose the category:
- CLAUDE.md rule: a behavioral instruction that should always be active (broad constraint or guideline)
- skill: specialized expertise loaded on-demand (domain-specific, not always needed)
- command: a repeatable step-by-step workflow with clear inputs/outputs
Return ONLY a JSON array, no prose outside it:
[
{{
"pattern": "short description of the recurring intent (max 60 chars)",
"category": "CLAUDE.md rule",
"suggested_name": "kebab-case-name",
"rationale": "one sentence explaining why this should be extracted",
"frequency": "high",
"example_messages": ["example 1", "example 2"],
"suggested_content": "what the skill/command/rule would contain (2-3 sentences)"
}}
]
Rules:
- Only include genuinely recurring patterns (at least 2 messages with similar intent)
- Prefer specific, actionable suggestions over generic ones
- Maximum 15 suggestions, sorted by impact (most valuable first)"""
def call_claude_cli(messages_batch: List[Dict], model: str) -> List[Dict]:
"""
Call the local `claude --print` CLI (uses your existing subscription).
No API key required.
"""
import subprocess
import tempfile
prompt = build_analysis_prompt(messages_batch)
# claude --print accepts the prompt as a positional argument
cmd = ['claude', '--print', prompt]
if model:
cmd += ['--model', model]
# Remove CLAUDECODE so the subprocess isn't blocked by nested-session detection
env = os.environ.copy()
env.pop('CLAUDECODE', None)
env.pop('CLAUDE_CODE_ENTRYPOINT', None)
try:
result = subprocess.run(
cmd,
env=env,
capture_output=True,
text=True,
timeout=120,
)
except FileNotFoundError:
raise RuntimeError("'claude' CLI not found. Make sure Claude Code is installed and in PATH.")
except subprocess.TimeoutExpired:
raise RuntimeError("claude CLI timed out after 120s.")
if result.returncode != 0:
detail = (result.stderr or result.stdout)[:500]
raise RuntimeError(f"claude CLI error (exit {result.returncode}):\n{detail}")
text = result.stdout.strip()
# Catch runtime errors reported on stdout (returncode 0 but failed)
if text.lower().startswith('execution error') or text.lower().startswith('error:'):
stderr_hint = f"\nstderr: {result.stderr[:300]}" if result.stderr.strip() else ""
raise RuntimeError(f"claude CLI reported an error:\n{text[:300]}{stderr_hint}")
# Strip markdown code fences if present
if text.startswith('```'):
text = re.sub(r'^```(?:json)?\n?', '', text)
text = re.sub(r'\n?```$', '', text)
# Extract JSON array if surrounded by prose
match = re.search(r'\[.*\]', text, re.DOTALL)
if match:
text = match.group(0)
try:
suggestions = json.loads(text)
except json.JSONDecodeError as e:
raise RuntimeError(f"Failed to parse CLI response as JSON: {e}\nRaw: {text[:500]}") from e
if not isinstance(suggestions, list):
raise RuntimeError(f"Expected JSON array, got: {type(suggestions)}")
return suggestions
def cmd_discover_llm(
project_dirs: List[Path],
since: str = '90d',
top: int = 15,
model: str = LLM_DEFAULT_MODEL,
json_output: bool = False,
):
"""LLM-powered pattern discovery via `claude --print` (uses your subscription)."""
since_dt = parse_duration(since)
print(f"Scanning sessions since {since_dt.strftime('%Y-%m-%d')}...", file=sys.stderr)
sessions_data = collect_sessions_data(project_dirs, since_dt)
if not sessions_data:
print("No sessions found in the given time range.", file=sys.stderr)
return
print(f"Collected {len(sessions_data)} sessions — deduplicating messages...", file=sys.stderr)
deduped = deduplicate_messages_for_llm(sessions_data, max_messages=300)
if not deduped:
print("No user messages found.", file=sys.stderr)
return
batch = deduped[:LLM_BATCH_SIZE]
print(f"Sending {len(batch)} unique messages to claude --print ({model})...", file=sys.stderr)
try:
suggestions = call_claude_cli(batch, model)
except RuntimeError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
suggestions = suggestions[:top]
if json_output:
print(json.dumps(suggestions, indent=2))
return
total_sessions = len(sessions_data)
total_projects = len({sd['project'] for sd in sessions_data})
print()
print(f" cc-sessions discover --llm — {total_sessions} sessions · {total_projects} project(s) · {model}")
print()
by_category: Dict[str, List[Dict]] = defaultdict(list)
for s in suggestions:
by_category[s.get('category', 'command')].append(s)
category_order = ['CLAUDE.md rule', 'skill', 'command']
category_icons = {'CLAUDE.md rule': '📋', 'skill': '🧩', 'command': ''}
for cat in category_order:
items = by_category.get(cat, [])
if not items:
continue
icon = category_icons.get(cat, '')
print(f" {icon} {cat.upper()}")
print(f" {'' * 70}")
for item in items:
freq = item.get('frequency', '')
freq_tag = f" [{freq}]" if freq else ""
print(f" {item.get('pattern', '?')}{freq_tag}")
print(f" -> /{item.get('suggested_name', '?')}")
print(f" {item.get('rationale', '')}")
if item.get('suggested_content'):
print(f" Content: {item['suggested_content']}")
for ex in item.get('example_messages', [])[:2]:
print(f" e.g. \"{ex[:100].replace(chr(10), ' ')}\"")
print()
print()
print(f" Run with --json to pipe to jq.")
print()
# ─── main ─────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="Search Claude Code session history")
parser.add_argument('--all', action='store_true', help="Search all projects")
@ -467,10 +1157,36 @@ def main():
# reindex
subparsers.add_parser('reindex', help="Force rebuild index")
# discover
discover_parser = subparsers.add_parser(
'discover',
help="Analyze sessions to suggest skills, commands, and CLAUDE.md rules",
)
discover_parser.add_argument(
'--since', default='90d',
help="Time window to analyze (default: 90d)",
)
discover_parser.add_argument(
'--min-count', type=int, default=3,
help="Minimum occurrences to surface a pattern (default: 3)",
)
discover_parser.add_argument(
'--top', type=int, default=20,
help="Maximum number of suggestions to show (default: 20)",
)
discover_parser.add_argument(
'--llm', action='store_true',
help="Use 'claude --print' for semantic analysis (uses your subscription)",
)
discover_parser.add_argument(
'--model', default=LLM_DEFAULT_MODEL,
help="Claude model to use with --llm, e.g. 'haiku' or 'sonnet' (default: CLI default)",
)
args = parser.parse_args()
# Get project dirs
if args.command in ['search', 'recent']:
if args.command in ['search', 'recent', 'discover']:
project_dirs = get_project_dirs(args.all)
if not project_dirs:
@ -492,6 +1208,23 @@ def main():
cmd_resume(args.session_id)
elif args.command == 'reindex':
cmd_reindex()
elif args.command == 'discover':
if args.llm:
cmd_discover_llm(
project_dirs,
since=args.since,
top=args.top,
model=args.model,
json_output=args.json,
)
else:
cmd_discover(
project_dirs,
since=args.since,
min_count=args.min_count,
top=args.top,
json_output=args.json,
)
if __name__ == '__main__':

View file

@ -796,7 +796,7 @@ You: /exit
Session ID: abc123def (saved for resume)
```
> **Session Search Tools**: For fast session search, see [session-search.sh](../examples/scripts/session-search.sh) (bash, lightweight) and [cc-sessions.py](../examples/scripts/cc-sessions.py) (Python, advanced features: incremental index, partial ID resume, branch filter). Also: [Observability Guide](./ops/observability.md#session-search--resume).
> **Session Search Tools**: For fast session search, see [session-search.sh](../examples/scripts/session-search.sh) (bash, lightweight) and [cc-sessions.py](../examples/scripts/cc-sessions.py) (Python, advanced features: incremental index, partial ID resume, branch filter, and `discover` for automated pattern analysis — [GitHub](https://github.com/FlorianBruniaux/cc-sessions)). Also: [Observability Guide](./ops/observability.md#session-search--resume).
**Common use cases**:
@ -884,6 +884,70 @@ Claude: [Resumes with Serena's persistent project understanding]
> **Source**: [DeepTo Claude Code Guide - Context Resume Functions](https://cc.deeptoai.com/docs/en/best-practices/claude-code-comprehensive-guide)
### Session Pattern Discovery (cc-sessions discover) {#session-pattern-discovery}
Your session history is a data source. Every time you ask Claude to do the same kind of thing across multiple sessions, that's a signal: extract it as a skill, command, or CLAUDE.md rule and stop paying the context tax on every request.
`cc-sessions discover` automates this analysis. It reads your session history, finds recurring patterns in user messages, and tells you what to extract.
**Install**:
```bash
curl -sL https://raw.githubusercontent.com/FlorianBruniaux/cc-sessions/main/cc-sessions \
-o ~/.local/bin/cc-sessions && chmod +x ~/.local/bin/cc-sessions
```
**Two modes**:
| Mode | How | Cost | Speed |
|------|-----|------|-------|
| N-gram (default) | Tokenizes messages, builds frequency index of 3-6 word phrases | Free, local | ~3s for 12 projects |
| `--llm` | Deduplicates messages, sends batch to `claude --print` | Uses your subscription | ~15s |
```bash
# N-gram mode: all projects, last 90 days
cc-sessions --all discover
# Lower threshold, narrower window
cc-sessions --all discover --since 60d --min-count 2 --top 15
# Semantic analysis via claude --print
cc-sessions --all discover --llm
# JSON output for scripting
cc-sessions --all discover --json | jq '.[] | select(.category == "skill")'
```
**Example output**:
```
cc-sessions discover — 847 sessions · 12 project(s) · since 90d
📋 CLAUDE.md RULE
────────────────────────────────────────────────────────────
write tests before implementation
234 sessions (28%) · 891 occurrences · score 0.416
→ 3a72f1c4-...
🧩 SKILL
────────────────────────────────────────────────────────────
security review authentication flow
71 sessions (8%) · 203 occurrences · score 0.084
→ 9f1c3a22-...
⚡ COMMAND
────────────────────────────────────────────────────────────
generate prisma migration rollback script
18 sessions (2%) · 44 occurrences · score 0.021
→ 44aab71c-...
```
**The 20% rule built into scoring**: patterns above 20% of sessions become `CLAUDE.md rule` suggestions (always load), 5-20% become `skill` suggestions (load on demand), below 5% become `command` suggestions (explicit invocation). The cross-project bonus (1.5×) prioritizes patterns that recur across different codebases — those are worth extracting even at lower frequency.
See also: [§5.1 Understanding Skills](#51-understanding-skills) for the distinction between CLAUDE.md rules, skills, and commands, and the [20% rule](#the-20-rule) for the decision framework.
**GitHub**: [FlorianBruniaux/cc-sessions](https://github.com/FlorianBruniaux/cc-sessions)
### Session Auto-Rename
When running multiple Claude Code sessions in parallel (split terminals, WebStorm tabs, parallel workstreams), the `/resume` picker shows sessions by timestamp or truncated first prompt — impossible to distinguish at a glance.
@ -1666,6 +1730,8 @@ When context gets high:
- Loses all context
- Use when changing topics
> **"One Task, One Chat"** — mixing unrelated topics across turns degrades model accuracy by ~39%. Context accumulates noise ("context rot") that distorts judgment even when total token usage stays low. Use `/clear` aggressively between distinct tasks, not just when the context bar turns red.
**Option 3: Summarize from here** (v2.1.32+)
- Use `/rewind` (or `Esc + Esc`) to open the checkpoint list
- Select a checkpoint and choose "Summarize from here"
@ -2139,6 +2205,62 @@ response = client.messages.create(
> Docs: [prompt caching](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching)
#### How Claude Code Handles Caching Automatically
Claude Code manages prompt caching without any configuration on your part. Understanding the mechanics helps you make decisions that keep cache hit rates high and costs low.
**Cache prefix hierarchy**
Every API call Claude Code makes structures content in this fixed order: `tools → system → messages`. Cache matching always starts from the beginning of this prefix. A stable tool list + stable CLAUDE.md + growing conversation history means the first two layers are almost always cache hits, while only new message turns require fresh computation.
**The 20-block lookback — the long-session trap**
Cache matching uses a bounded lookback of approximately 20 blocks. In a long session with many tool calls and exchanges, blocks from early in the conversation fall outside this window and become cache misses. Practical consequence: very long sessions gradually lose cache efficiency at the message layer. The fix is `/compact` — it compresses the conversation history into a single summary block, resetting the lookback window and restoring high hit rates.
**Minimum token thresholds by model**
A block must meet a minimum size to be eligible for caching. Blocks smaller than the threshold are never cached, regardless of how stable they are:
| Model family | Minimum tokens |
|---|---|
| Claude Opus 4.6, Opus 4.5, Haiku 4.5 | 4,096 |
| Claude Sonnet 4.6 | 2,048 |
| Claude Sonnet 4.5, Sonnet 4, Sonnet 3.7, Opus 4.1, Opus 4 | 1,024 |
| Claude Haiku 3.5, Haiku 3 | 2,048 |
Short CLAUDE.md files (under ~1,000 tokens) may not be cached at all on Sonnet models. If cost optimization matters, make sure your system prompt crosses the threshold for your target model.
**Tool result size and cache economics**
Tool results land in the message history and stay there for the rest of the session. Every subsequent API call re-reads that history — at cache read price (0.1x), but still proportional to size. A `git status` output of 500 tokens costs 500 × 0.1x to read on every turn that follows. The same output at 50 tokens (filtered by a tool like RTK) costs 50 × 0.1x — 90% less, compounding across every turn in the session. Compact tool outputs are not just faster to process; they make the entire cached prefix cheaper to maintain.
The same logic applies to cache writes: a smaller history prefix means cheaper initial writes (1.25x × fewer tokens).
**Monitoring cache performance in your own pipelines**
When building agents or pipelines on top of the Anthropic API, the response `usage` object exposes cache metrics directly:
```python
response = client.messages.create(...)
print(response.usage.cache_creation_input_tokens) # Tokens written to cache this request
print(response.usage.cache_read_input_tokens) # Tokens read from cache (hits)
print(response.usage.input_tokens) # Non-cached input tokens
```
Calculate your hit rate as `cache_read / (cache_read + cache_creation)` across requests. A ratio above 0.8 means your prompt structure is working well. Low ratios usually mean content in the stable prefix is changing between requests — check for timestamps, random IDs, or dynamic content embedded in your system prompt.
No dedicated monitoring tool exists specifically for Claude Code session cache metrics. Cost tracking via `ccusage` covers overall spend but does not break out cache hit rates. For cache-specific visibility in custom pipelines, parse the response fields above.
**Practical rules**
- Keep CLAUDE.md stable between sessions — edits invalidate the system cache one-shot, then it re-warms on the next request
- Run `/compact` before the conversation gets very long, not after performance degrades
- Avoid dynamic content in stable sections (dates, random values, per-request context)
- Larger CLAUDE.md = more expensive cache write, but also more tokens saved per read — profitable after ~2 hits
> Docs: [prompt caching](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching)
#### Tracking Costs
**Real-time tracking**:
@ -6820,7 +6942,9 @@ Is this a repeatable workflow with steps?
└─ No → Just write it in CLAUDE.md as instructions
```
> **See also**: [§2.7 Configuration Decision Guide](#27-configuration-decision-guide) for a broader decision tree covering all seven mechanisms (including Hooks, MCP, and CLAUDE.md vs rules).
> **The 20% rule**: if an instruction applies to more than 20% of your conversations, put it in `CLAUDE.md` (always loaded). If it applies to fewer than 20%, make it a skill (loaded on demand). The difference matters for token efficiency: a skill's system prompt is injected only when Claude invokes it, while CLAUDE.md content counts against every request's context window.
> **See also**: [§2.7 Configuration Decision Guide](#27-configuration-decision-guide) for a broader decision tree covering all seven mechanisms (including Hooks, MCP, and CLAUDE.md vs rules). To automate detection of what belongs in each category, use [`cc-sessions discover`](#session-pattern-discovery) — it applies this 20% threshold to your actual session history.
#### Common Patterns
@ -10385,6 +10509,25 @@ grepai search "session creation logic"
> **Source**: [grepai GitHub](https://github.com/yoanbernabeu/grepai)
**Blast-Radius Pattern (pre-refactoring workflow):**
Before modifying any widely-used function, run a dependency query to enumerate all affected call sites — then decide whether to proceed. This named workflow prevents cascading breakage in large codebases.
```bash
# Step 1: Map all callers before touching a function
grepai trace callers "processPayment"
# → Returns: 14 call sites across 7 files
# Step 2: Check callees (what it depends on)
grepai trace callees "processPayment"
# → Returns: 3 downstream dependencies
# Step 3: Decide scope before writing a single line
# 14 callers + 3 deps = significant blast radius → plan the refactor first
```
Run this before starting any refactor touching a function used in 3+ places — not after hitting compile errors.
---
### claude-mem (Automatic Session Memory)
@ -11746,6 +11889,22 @@ serena # large codebase
Community tools (e.g. [cc-setup](https://github.com/rhuss/cc-setup)) are emerging to provide a TUI registry with per-project toggling and health checks — useful if you manage 8+ servers regularly.
#### MCP Tool Search — Lazy-Loading at Scale
Claude Code v4 introduced **MCP Tool Search**: instead of loading all MCP tool definitions at startup, tool schemas are fetched on-demand when Claude needs them.
**Why it matters**: each MCP server injects its full tool schema into the context window. With a dozen servers, that's ~77,000 tokens consumed before you've written a single prompt.
| Setup | Context used by tools |
|-------|----------------------|
| All tools loaded upfront | ~77,000 tokens |
| MCP Tool Search enabled | ~8,700 tokens |
| **Reduction** | **~85%** |
Model accuracy on tool-selection tasks (measured on Opus 4): 49% → 74% (+25 points) when switching from full preload to lazy-loading. Auto-enables when MCP tools would consume >10% of the context window.
**Practical implication**: you can now connect dozens of MCP servers without the "too many tools" accuracy penalty. The advice to keep global config minimal still applies for unrelated tools, but MCP Tool Search changes the calculus for large project-specific sets.
### CLI-Based MCP Configuration
**Quick setup with environment variables**:
@ -13828,6 +13987,63 @@ We've made your experience faster and more personal:
- Solution: Clean up commit history before generation (interactive rebase)
- Better: Enforce commit message format with git hooks
### Changelog Fragments: Per-PR Enforcement Pattern
An alternative to generating release notes from commits is to capture the context _while implementing_, not at release time. The "changelog fragments" pattern replaces a shared `CHANGELOG.md` with one YAML file per PR, accumulated in `changelog/fragments/`, assembled automatically at release.
**The core problem with commit-based approaches**: by the time you run `git log` to generate release notes, context is gone. The developer who fixed a race condition three weeks ago is the only one who understood the impact. The commit message says `fix SSE handling`.
The fragments pattern solves this with 3 enforcement layers:
**Layer 1 — CLAUDE.md rule**: Load a `git-workflow.md` rule that encodes the full fragment workflow. When a developer asks Claude Code to "create the PR," it reads the diff, infers type/scope/title, generates the YAML, validates it, and commits it as part of the branch. Claude handles it autonomously.
```yaml
# changelog/fragments/886-fix-visiochat-sse-race-condition.yml
pr: 886
type: fix
scope: "visiochat"
title: "Fix empty chat after starting activity due to SSE race condition"
description: |
SSE workplan fires before AI stream completes, causing ChatWrapper to mount
with 0 messages. Added isStartingActivityRef guard and await response.text().
breaking: false
migration: false
```
**Layer 2 — `UserPromptSubmit` hook**: Detects PR creation intent and checks whether the fragment was already mentioned.
```bash
# Tier 0 enforcement in smart-suggest.sh
if echo "$PROMPT_LC" | grep -qE '(create.*pr|make.*pr|pull.?request)'; then
if ! echo "$PROMPT_LC" | grep -qE '(changelog|fragment|skip-changelog)'; then
suggest "pnpm changelog:add" "REQUIRED before merge — fragment missing"
else
suggest "/pr" "PR creation with structured description"
fi
fi
```
The hook is non-blocking and shows one suggestion inline, before Claude processes the prompt. If the fragment is already mentioned, the hook stays silent and suggests the normal PR command.
**Layer 3 — CI gate**: Two independent GitHub Actions jobs. The first validates fragment existence and structure. The second checks that `migration: true` is set if the PR adds SQL migration files — this job runs regardless of bypass labels, because a "skip-changelog" PR can still add a migration that the deployment team needs to know about.
**Assembly at release:**
```bash
pnpm changelog:assemble --version 1.8.0 [--dry-run]
```
Reads all fragments, groups by type, inserts a versioned section into `CHANGELOG.md` replacing a `## [Next Release]` placeholder, archives fragments to `changelog/fragments/released/{version}/`.
**Benefits over commit-based generation:**
- Zero merge conflicts (each fragment is a unique file per PR)
- Context written at implementation time, not reconstructed later
- DB migrations surfaced explicitly in every fragment
- Bypass is auditable (closed label list visible in PR history)
Full workflow documentation: [Changelog Fragments](./workflows/changelog-fragments.md)
Hook reference implementation: [`examples/hooks/bash/smart-suggest.sh`](../examples/hooks/bash/smart-suggest.sh)
### Deployment Automation
Claude Code can automate deployments to Vercel, GCP, and other platforms using stored credentials. The key is assembling three components: secret management, a deploy skill, and mandatory guardrails.
@ -15999,6 +16215,39 @@ You: "Fix typos in auth.ts, user.ts, and api.ts"
# Single context load, multiple fixes
```
**6. Pre-structural indexing:**
Instead of letting Claude read files on demand throughout a session, pre-build a structural index of your codebase before starting. Claude queries the index (1 call) rather than reading files sequentially (5-10 reads per task).
```bash
# With CodeXRay (npx setup, SQLite-backed, 15 languages):
npx codexray # Interactive setup + first index build
cxr watch & # Background sync on file changes
# Claude Code then queries the graph instead of reading files:
# "find the payment module" → 1 graph query vs 5-10 file reads
```
Tools built on this pattern replace 5-10 file reads with 1 structured query — roughly 75% fewer tool calls for discovery tasks.
**Dead code and circular dependency detection:**
A structural index also enables analysis that file-by-file reading cannot surface efficiently:
- **Dead code**: Functions defined but never called — safe to delete, reducing future context noise
- **Circular dependencies**: Module A imports B imports A — architectural debt that silently inflates Claude's reasoning overhead
- **Hotspots**: Files with the highest dependency count — prioritize for documentation or refactoring first
```bash
# With grepai (zero callers = dead code candidate):
grepai trace callers "MyFunction" # Empty result → safe to investigate for deletion
# With a structural MCP tool (if available):
# Tools like CodeXRay expose: codexray_deadcode, codexray_circular, codexray_hotspots
```
> **Community tools**: [CodeXRay](https://github.com/NeuralRays/codexray) (Tree-sitter + SQLite, 16 MCP tools, 15 languages) and [Claudette](https://github.com/nicmarti/Claudette) (Go binary, 4 languages) are early implementations of this approach. Both are alpha-stage as of March 2026 — use grepai for production workflows.
### Command Output Optimization with RTK
**RTK (Rust Token Killer)** filters bash command outputs **before** they reach Claude's context, achieving 60-90% token reduction across git, testing, and development workflows. 446 stars, 38 forks, 700+ upvotes on r/ClaudeAI.
@ -16459,6 +16708,7 @@ Time savings from effective Claude Code usage typically far outweigh API costs f
├─ "I want to spec before code" ─────→ workflows/spec-first.md
├─ "I need to plan architecture" ────→ workflows/plan-driven.md
├─ "I'm iterating on something" ─────→ workflows/iterative-refinement.md
├─ "Feasibility is unknown" ─────────→ workflows/rpi.md
└─ "I need methodology theory" ──────→ methodologies.md
```

View file

@ -111,7 +111,9 @@ deep_dive:
# Session management
session_search: "guide/ops/observability.md:38"
session_search_script: "examples/scripts/session-search.sh"
cc_sessions_script: "examples/scripts/cc-sessions.py"
cc_sessions_script: "examples/scripts/cc-sessions.py" # 1225-line full version with discover subcommand
cc_sessions_github: "https://github.com/FlorianBruniaux/cc-sessions"
cc_sessions_discover: "guide/ultimate-guide.md#session-pattern-discovery" # n-gram + --llm pattern analysis
session_resume_limitations: "guide/ops/observability.md:126"
session_cross_folder_migration: "guide/ops/observability.md:126"
session_migration_manual: "guide/ops/observability.md:126"