Scanning & Discovery
The scanner automatically discovers lessons from your session history. It runs in the background on every session startup and writes candidates to the database for your review.
How scanning works
Claude Code writes every session to a JSONL file in ~/.claude/projects/. Each line is a JSON object — a user message, an assistant response, a tool call, or a tool result.
The scanner reads these files and looks for structured tags and heuristic patterns across four tiers:
Background scan
On every session startup, session-start-scan.mjs fires and spawns lessons.mjs scan --auto as a detached background process. The parent process unrefs the child immediately so session startup is not delayed.
The scan:
- Reads saved byte offsets from
data/scan-state.jsonto resume where it left off - Processes only new bytes in each JSONL file (incremental)
- Writes new candidates to the database with
status='candidate' - Updates
data/scan-state.jsonwith new offsets
The background scan respects autoScanIntervalHours — if the last scan was recent, it exits early.
Running a scan manually
The background scan already runs automatically on session startup, so you rarely need to trigger one yourself. If you want to force a fresh scan before reviewing, run:
/lessons:review
/lessons:review runs the scanner and walks you through whatever it finds — see Interactive review below.
CLI equivalent
node scripts/lessons.mjs scan # incremental scan, interactive
node scripts/lessons.mjs scan --auto # non-interactive (background mode)
node scripts/lessons.mjs scan --full # reset offsets, re-scan everything
node scripts/lessons.mjs scan --dry-run # show what would be found, don't write
Tier 1 — Structured tags
When Claude makes and corrects a mistake during a session, it emits a #lesson tag:
#lesson
tool: Bash
trigger: git stash
problem: git stash only stashes tracked files — untracked files silently left behind
solution: Use `git stash -u` to include untracked files
tags: tool:git, severity:data-loss
#/lesson
The scanner greps for #lesson markers in assistant message lines, parses the block, and extracts a structured candidate with:
tool,trigger,problem,solution,tagsfrom the tagconfidencescored based on how many optional fields are presentsessionIdandmessageIdfor provenance
Tier 1 candidates auto-promote during interactive scan if they pass validation:
problemandsolutioneach ≥ 20 chars- No template placeholders
- Jaccard similarity vs existing lessons < 0.5
If they fail validation, they appear in the review queue for manual adjustment.
Tier 2 — Heuristic detection
For sessions where Claude didn't emit #lesson tags, the heuristic detector applies a sliding window over the JSONL stream looking for:
- A tool result that contains error signals (non-zero exit,
Error:,ECONNREFUSED, etc.) - An assistant response that follows with a correction (contains
instead,should,fix,use, etc.)
When both conditions match within a configurable window, the detector extracts a candidate with:
problemderived from the error messagesolutionderived from the correctionconfidencein the 0.4–0.6 range (lower than Tier 1) — always requires human confirmation before it's promoted
Tier 2 candidates always require manual review before promotion. They're noisier and need a summary, trigger pattern, and confirmation that the mistake is real and reusable.
Tier 3 — Structural detection
Tier 3 captures insight announcements — structured patterns like "I see that...", "The issue is...", or explicit problem/solution headers in assistant responses. These are written to pending_semantic_windows in the DB and surfaced for manual review. They do not auto-promote.
Tier 4 — LLM deep scan
Tier 4 runs a full-session LLM analysis using the Anthropic API to extract candidates that none of the pattern-based tiers would catch. This tier requires ANTHROPIC_API_KEY to be set in the environment before Claude Code starts.
The session-start-scan.mjs hook only launches the deep-scan background job if ANTHROPIC_API_KEY is set. Without it, Tiers 1–3 still run normally.
Setting up the API key
Two options — use whichever fits your setup:
Option A — environment variable (key available system-wide):
export ANTHROPIC_API_KEY=sk-ant-...
Add to your shell profile (~/.zshrc, ~/.bashrc) so it's set before Claude Code starts.
Option B — data/.api-key file (scoped to this plugin only, gitignored):
echo "sk-ant-..." > ~/lessons-learned/data/.api-key
The session-start hook reads this file and injects the key into the scan subprocess environment. It does not affect any other process. The file is listed in .gitignore and will not be committed.
Viewing candidates
/lessons:manage → "show pending candidates"
/lessons:review
CLI equivalent
node scripts/lessons.mjs scan aggregate # ranked JSON view of all candidates
node scripts/lessons.mjs list --status candidate # all candidates
scan aggregate (formerly scan candidates, now a deprecated alias for the same thing) ranks
every candidate by sessionCount * projectCount * confidence — it doesn't filter by session
count, so single-occurrence candidates still appear, just lower in the list.
Promoting candidates
/lessons:review is the recommended way to promote both Tier 1 and Tier 2 candidates — see Interactive review below. The CLI equivalents in this section exist for scripting and local development.
Tier 1 candidates
Auto-promoted during /lessons:review if they pass validation. Claude shows you a preview and you confirm or skip.
CLI equivalent
node scripts/lessons.mjs scan
Tier 2 candidates
Require manual review. /lessons:review prompts you for problem, solution, and trigger before promoting.
CLI equivalent
List candidates to find the id you want, then promote by ID —
scan promote <index> has been removed since indexes are recomputed on every listing:
node scripts/lessons.mjs scan aggregate # list candidates (includes each one's id)
node scripts/lessons.mjs promote --ids <id> # promote that candidate
To adjust fields at promotion time, pass --patch:
node scripts/lessons.mjs promote --ids <id> --patch '{"priority": 8, "tags": ["tool:git"]}'
After promotion, the manifest is rebuilt automatically.
Interactive review
/lessons:review
Walks through all pending candidates (Tier 1 and Tier 2) one at a time with guided prompts. It's the recommended way to review a batch of candidates.
Scan state
The scanner tracks progress in data/scan-state.json:
{
"files": {
"/Users/alice/.claude/projects/abc123.jsonl": 184320
},
"lastFullScanAt": "2026-04-01T00:00:00Z"
}
Each file entry is the last byte offset read. The next scan resumes from that point, processing only new content. This makes incremental scans fast even on large session archives.
scan-state.json is managed by the scanner — don't edit it directly. To force a full re-scan:
node scripts/lessons.mjs scan --full
Configuring scan paths
By default, the scanner reads ~/.claude/projects/. If your session files are elsewhere:
{
"scanPaths": ["~/.claude/projects/", "/path/to/other/agent/sessions/"]
}
Edit data/config.json to add directories. Tilde expansion is supported.
Scan frequency
The autoScanIntervalHours setting (default: 24) prevents the background scan from running on every session startup. A manual scan always runs regardless of this setting.
To scan on every startup:
{
"autoScanIntervalHours": 0
}
To scan weekly:
{
"autoScanIntervalHours": 168
}