Skip to main content

Working with Lessons

A lesson is a structured record of a mistake and its fix, annotated with trigger patterns that control when it fires.


Lesson anatomy

{
"id": "01JQSEED00000000000000001",
"slug": "pytest-tty-hanging-k9m2",
"summary": "pytest hangs in non-interactive envs due to TTY detection",
"problem": "Running bare `pytest` in Claude Code causes the process to hang waiting for TTY input.",
"solution": "Use `python -m pytest --no-header -p no:faulthandler`",
"type": "guard",
"toolNames": ["Bash"],
"commandPatterns": ["\\bpytest\\b(?!.*(--no-header|-p no:faulthandler))"],
"pathPatterns": [],
"tags": ["lang:python", "tool:pytest", "severity:hang"],
"priority": 8,
"confidence": 0.95
}

Core fields

FieldRequiredDescription
summaryyesOne-line description. Used as fallback injection when full text exceeds budget. Max 120 chars.
problemyesRoot cause explanation. Describes why something fails, not just that it does. Min 20 chars.
solutionyesConcrete fix. Actionable commands or code. Copy-pasteable. Min 20 chars.
typeyesdirective | guard | hint | protocol — see trigger types below.
toolNamesyesTools this lesson applies to. See trigger types below.
priorityyes1–10. Higher wins budget conflicts.
confidenceyes0.0–1.0. Below minConfidence (default 0.5), excluded from the manifest.

Trigger types

{
"type": "hint",
"toolNames": ["Bash"],
"commandPatterns": ["\\bpytest\\b(?!.*(--no-header))"],
"pathPatterns": ["**/*.test.py", "pytest.ini"]
}
TypeFires whenUse for
commandPatternsBash command matches regexTool-specific commands like pytest, git stash
pathPatternsRead/Edit/Write path matches globFile-type warnings like "don't edit this file directly"
toolNamesExact tool name matchBroad reminders for any use of a tool
type: 'protocol'Session startupCross-cutting reasoning reminders with no specific trigger

::: tip Use negative lookahead to suppress when fix is applied

"commandPatterns": ["\\bpytest\\b(?!.*(--no-header|-p no:faulthandler))"]

This pattern fires on pytest tests/ but not on pytest --no-header tests/ — suppressing injection once the fix is already in place. :::

Advanced fields

These fields give you finer control over when a lesson fires and whether it appears in the manifest at all.

modelPatterns

An array of regexes tested against the command or file path as an AND gate. When non-empty, the lesson only fires if at least one pattern matches. Use this to restrict a lesson to a specific model or provider.

{
"modelPatterns": ["o3", "o4-mini", "reasoning_effort"]
}

Pair with tags like model-version:o3 or provider:openai to make the intent explicit.

requires

Excludes a lesson from the manifest unless a specific artifact (plugin, MCP server, or skill) is installed. Accepts a single object or an array (OR logic — any match satisfies the requirement).

{ "requires": { "type": "plugin", "name": "serena" } }
{
"requires": [
{ "type": "plugin", "name": "serena" },
{ "type": "mcp-server", "name": "serena" }
]
}

Valid shapes: {"type":"plugin","name":"..."}, {"type":"mcp-server","name":"..."}, {"type":"skill","name":"..."}.

Use requires when a lesson only makes sense if the referenced tool is present — for example, a lesson about Serena's replace_content is useless if Serena isn't installed.

duplicatedBy

The inverse of requires. Excludes a lesson from the manifest when the named artifact IS installed. Accepts a single object (not an array).

{ "duplicatedBy": { "type": "plugin", "name": "serena" } }

Use this to suppress a workaround lesson once the real fix (the plugin that makes it unnecessary) is installed.

Priority guide

RangeMeaning
9–10Data loss, session hangs, security issues
7–8Common recurring mistakes, wrong defaults
4–6Good-to-know patterns
1–3Situational, low-frequency

Lesson types

The type field controls how a lesson affects tool calls:

TypeBehavior
hintInject as additionalContext on matching tool call
guardDeny the tool call entirely; message shown to the agent
protocolInject at session start (reasoning reminders)
directiveInject at session start and on matching tool calls

Guard lessons (blocking): set type: "guard" to deny a tool call entirely. The message field is shown to the agent as the denial reason. Use {command} in the message for a substituted snippet of the actual command (truncated to 120 chars):

type: guard
message: 'pytest without --no-header hangs. Rerun as: {command} --no-header -p no:faulthandler'

Use guard sparingly — only for commands with known data-loss or irreversible consequences.

Mid-session re-injection

directive and protocol lessons are injected once at session start, but their influence fades as the context window fills. To counteract this, a PostToolUse hook monitors context usage and re-injects them automatically at three thresholds:

InjectionThresholdRationale
First30%Pre-degradation — model is maximally receptive
Second52%Early rot zone — catch drift before it compounds
Third70%Deep rot — last refresh before auto-compaction at ~80%

When a threshold is crossed you'll see a ## [lessons-learned] Directive & Protocol Refresh block appear in context, formatted identically to the session-start injection. Each threshold fires at most once per session.

Fallback: if context percentage isn't available, the hook fires every 20 tool calls instead.

Both values are configurable — see Configuration → Re-injection settings for LESSONS_REINJECT_THRESHOLDS and LESSONS_REINJECT_TOOL_COUNT.


Adding a lesson

/lessons:add

Claude asks five questions conversationally: problem, solution, trigger, summary, and optional tags/priority. Takes ~2 minutes.

Validation rules

The CLI enforces these before writing:

  • summary, problem, solution each ≥ 20 characters
  • No unfilled template placeholders (<what_went_wrong> etc.)
  • summary must not end with ...
  • Trigger must not be a prose gerund (e.g. "running pytest")
  • Jaccard similarity of problem vs. all existing lessons < 0.5 (no near-duplicates)

Listing lessons

/lessons:manage → "show active"

/lessons:manage browses lessons across every status — candidates, active, and archived — and supports search, e.g. "find lessons about pytest".

CLI equivalent

node scripts/lessons.mjs list # formatted table
node scripts/lessons.mjs list --json # JSON array

Editing a lesson

/lessons:manage → "edit priority on pytest-tty-hanging to 9"

After editing an active lesson, the manifest is rebuilt automatically.

CLI equivalent

node scripts/lessons.mjs edit --id <id> --patch '{"priority": 9}'

Archiving and restoring

/lessons:manage → "archive pytest-tty-hanging — no longer relevant"
/lessons:manage → "restore pytest-tty-hanging"

/lessons:manage asks for a reason if you don't give one, shows a confirmation, then archives or restores the lesson and rebuilds the manifest.

CLI equivalent

node scripts/lessons.mjs promote --archive "<id>:reason here" # archive
node scripts/lessons.mjs restore --ids <id> # restore

The seed lesson store

The plugin ships with 30 hand-authored lessons covering common failure patterns:

CategoryExamples
Pythonpytest TTY hang, mock.patch namespace, pip venv targeting
JavaScript/Nodevitest parallel isolation, npm link peer deps, Node.js deprecation warnings
Gitgit stash untracked files, heredoc commit messages
Browser/CDPChrome DevTools ECONNREFUSED, async eval returning undefined
Shelloh-my-zsh NVM warnings, pre-commit hook failures, Biome v2 config schema

Review the seed lessons in /lessons:manage → "show active" to see what's already covered.