lesson
Node
lessons-learned · Claude Code plugin · session-aware · zero build step

Every mistake, once.
Permanent memory for Claude.

lessons-learned intercepts every tool call during a Claude Code session. Before Claude runs a command or edits a file, matching lessons inject as context warnings — right when they're needed. Each corrected mistake gets stored and prevents the next occurrence.

⚠ The Problem

Claude Code sessions are stateless. Every mistake corrected in one session is forgotten by the next. Over long agentic work, the same classes of errors recur — wasting tokens, time, and trust. There's no mechanism to carry hard-won corrections forward.

✦ The Solution

lessons-learned intercepts every tool call via Claude Code hooks. Before Claude runs a command or edits a file, matching lessons inject as context warnings. Guards can block dangerous commands outright. Directives re-inject at 30/52/70% context usage to fight context rot.

◈ The Payoff

Each corrected mistake prevents future repetitions. The lesson store compounds over time, shaped by your actual workflow. Lessons persist across sessions, projects, and agents. Context rot reversal — knowledge that outlasts any single conversation.

7 hooks
·
4 scanning tiers
·
<10ms match latency
·
3-layer dedup
·
5 lesson statuses
·
4 lesson types

System Property Graph

The complete data model — how session JSONL files become candidates, lessons, and runtime injections. Click any node to inspect its properties. Edges show data flow and relationships.

Node types
Edge types

Data flow summary: Session JSONL files (written by Claude Code) are scanned at each session start by session-start-scan.mjs. The four scanning tiers (structured → heuristic → structural → LLM) extract candidates and write them to data/lessons.db (SQLite). The lessons promote CLI command moves candidates to active status. lessons build compiles active lessons into data/lesson-manifest.json — the runtime-only file loaded by every PreToolUse hook call. data/config.json embeds its settings into the manifest at build time. data/scan-state.json tracks byte offsets so each scan is incremental.

How Hooks Fire During a Session

Swimlane showing the full event sequence across four actors. Claude Code fires hooks automatically — no manual invocation needed. Hover nodes for details.

Hook wiring: All hooks are declared in hooks/hooks.json and registered with Claude Code automatically on plugin install. The matcher field is a regex tested against the session event type (e.g., startup|resume|clear|compact) or tool name (e.g., Read|Edit|Write|Bash|Glob). Multiple hooks can match the same event — they run sequentially.

Status State Machine

Every lesson begins as a candidate extracted by the scanner and follows a review-promote workflow before injection. Click any state to inspect it.

candidate

Raw candidate extracted by scanner. Not yet validated or reviewed. Stored in lessons.db with status='candidate'.

Status meanings: candidate — extracted, not validated. reviewed — passed lessons review validation gates. active — compiled into manifest and injected at PreToolUse. disabled — soft-disabled; still in DB but excluded from manifest. archived — permanently removed; retained for history only. Use lessons promote --ids to advance status; lessons edit --patch '{"disabled":true}' to soft-disable.

Inside pretooluse-lesson-inject.mjs

Every tool call (Read, Edit, Write, Bash, Glob) passes through this 6-stage pipeline in under 10ms. Stages run sequentially from hook entry to inject-or-block. Click any node to see what it does and what it reads/writes.

Press Play to walk through the injection pipeline step by step. Click any node for full properties.

Matching Algorithm

matchLessons() in core/match.mjs evaluates each lesson in the manifest against the current tool call in this order:

StepCheckFail behavior
1. toolNames Lesson's toolNames array must include the current tool (e.g. "Bash"). Required — a lesson without toolNames can never fire. Skip lesson entirely
2. scope If lesson.scope is set, cwd-derived projectId must match. Global lessons (scope=null) always pass. Skip lesson
3. commandRegexSources For Bash tools: at least one regex in commandRegexSources must match. If commandMatchTarget="executable", quoted strings are stripped first to prevent trigger on --patch values. Lesson not matched yet
4. pathRegexSources For file tools (Read/Edit/Write): at least one regex in pathRegexSources must match file_path. Lesson not matched yet
5. Tool-only match If lesson has no commandRegexSources or pathRegexSources, toolName match alone is sufficient (e.g. MCP tool lessons).
6. modelRegexSources AND gate: if non-empty, at least one must match command OR file_path. Used to gate model-specific lessons (e.g. o3, o4-mini). Matched → not matched
7. disabled lesson.disabled=true is always excluded regardless of other fields. Skip lesson

3-Layer Dedup System

Before injection, lessons are filtered through three dedup layers to prevent the same lesson from firing repeatedly in a session, even across parallel subagents.

Layer 1

Env Var

LESSONS_SEEN — a comma-separated list of seen slugs passed between hook invocations as an environment variable. Fast within a single agent chain.

Layer 2

Session Temp File

$TMPDIR/lessons-{hash}-seen.txt — persists the seen set across subagents and multiple hook invocations in the same session.

Layer 3

O_EXCL Claim Dir

$TMPDIR/lessons-{hash}-seen.d/{slug} — atomic O_EXCL file creation. Prevents duplicate injection when two agents race to inject the same lesson concurrently.

How Sessions Become Candidates

At every session start, a background process scans prior JSONL logs through four tiers of increasing depth. Each tier writes candidates to lessons.db. Click stages to animate.

All 7 Hooks

Complete reference for every hook wired in hooks/hooks.json. Timeouts, matchers, and exact behavior for each hook file.

Lesson Types & Injection Behavior

TypeWhen injectedEffectExit code
hint PreToolUse — on trigger match Prepends warning to Claude's context as additionalContext inside a <details> block 0 (allow)
guard PreToolUse — on trigger match Blocks the tool call entirely. Outputs reason string to Claude as the denial explanation. 2 (block)
protocol SessionStart only Injected once at session start as session-level guidance. Not injected at tool calls. 0
directive SessionStart + re-injected at 30/52/70% context usage Non-negotiable rules. Highest priority. Re-injected by posttooluse-directive-reinject.mjs. 0

Install in 4 Steps

Zero config required. The plugin wires itself into Claude Code via hooks.json automatically. All scanning is incremental; lesson injection adds under 10ms per tool call.