Skip to main content

Contributing Lessons

This guide covers writing, reviewing, and tuning lessons for maximum effectiveness.

Lesson types

TypeWhen injectedEffect
hintPreToolUse — on trigger matchPrepends warning to Claude's context
guardPreToolUse — on trigger matchBlocks the tool call + injects reason
protocolSessionStartInjected once at session start
directiveSessionStartAlways-on; higher priority than protocol

Rule of thumb: default to hint. Use guard only for hard stops (destructive commands, security violations). Use protocol for once-per-session reminders. Use directive only for absolute always-on rules that need no trigger.

One concrete example per type

hint — triggered warning on a specific command:

{
"summary": "git stash silently drops untracked files without -u flag",
"problem": "Running `git stash` without the `-u` flag silently leaves untracked files behind, risking data loss when the stash is applied elsewhere.",
"solution": "Use `git stash -u` or `git stash --include-untracked` to include untracked files in the stash.",
"type": "hint",
"tool": "Bash",
"commandPatterns": ["\\bgit\\s+stash\\b"],
"tags": ["tool:git", "severity:data-loss"],
"priority": 7
}

guard — blocks a dangerous command pattern:

{
"summary": "never eval user-supplied strings — use bash -c or a wrapper instead",
"problem": "Using `eval \"$user_input\"` executes arbitrary shell code. Even trusted input can contain subshell constructs or variable expansions that produce unintended side effects.",
"solution": "Use `bash -c \"$1\"` with positional parameters, or wrap the command in an array and pass to exec. Never interpolate user input directly into eval.",
"type": "guard",
"tool": "Bash",
"commandPatterns": ["\\beval\\b"],
"commandMatchTarget": "executable",
"tags": ["severity:security", "topic:shell"],
"priority": 9
}

protocol — once-per-session reminder injected into the workspace:

{
"summary": "emit #lesson tags when you discover a problem→solution sequence",
"problem": "Insights found during a session are lost unless captured. Future sessions repeat the same mistakes because there is no persistent record of what went wrong and how it was fixed.",
"solution": "Whenever you identify a problem→solution pair, emit a #lesson...#/lesson block in your response. The scanner picks it up and promotes it to the lesson store after the session.",
"type": "protocol",
"tags": ["topic:lesson-capture", "severity:process"],
"priority": 8
}

directive — always-on rule, no trigger needed:

{
"summary": "always check serial console first when a Pi node fails to boot",
"problem": "SSH and ping give no diagnostic signal when a node fails before the network stack comes up. Checking SSH/ping wastes time and returns nothing useful at that stage.",
"solution": "Connect via serial console (screen /dev/cu.usbmodem<ID> 115200) immediately when a node is unreachable. Serial shows BOOT_ORDER and the exact failure point before any network is available.",
"type": "directive",
"tags": ["env:pi-cluster", "topic:debugging"],
"priority": 6
}

Field reference by type

Fieldhintguardprotocoldirective
summaryrequiredrequiredrequiredrequired
problemrequiredrequiredrequiredrequired
solutionrequiredrequired (≥20 chars)requiredrequired
toolrequiredrequiredmust be emptymust be empty
commandPatternsoptionaloptionalignoredignored
pathPatternsoptionaloptionalignoredignored
commandMatchTargetoptionalrecommended
tagsoptionaloptionaloptionaloptional
priorityoptional (default 5)optional (default 5)optional (default 5)optional (default 5)
confidenceoptional (default 0.8)optional (default 0.8)optional (default 0.8)optional (default 0.8)
scopeoptionaloptionaloptionaloptional

The directive vs hint distinction

This is the most common misconfiguration:

Wrong — directive with toolNames (toolNames silently ignored):

{
"type": "directive",
"tool": "Bash",
"commandPatterns": ["\\bgit\\b"]
}

directive injects at session start regardless of any tool call. toolNames is silently ignored. The lesson fires on every session whether or not git is ever used.

Right — use hint when you want trigger-scoped injection:

{
"type": "hint",
"tool": "Bash",
"commandPatterns": ["\\bgit\\b"]
}

Right — use directive when the lesson should always be active (no conditions):

{
"type": "directive"
}

The intake validator (lessons add) will block directive/protocol lessons that have tool set.

Dead trigger: the silent no-fire trap

A hint or guard with commandPatterns or pathPatterns but no tool can never fire. The matching engine bails at step 1 (toolNames check) before evaluating patterns.

Wrong:

{
"type": "hint",
"commandPatterns": ["\\bosascript\\b"]
}

Right:

{
"type": "hint",
"tool": "Bash",
"commandPatterns": ["\\bosascript\\b"]
}

The intake validator blocks this misconfiguration.

commandMatchTarget: "executable"

By default, commandPatterns matches against the full command string including all arguments. This can cause false positives when your trigger word appears inside a JSON --patch argument or a quoted string.

Example:

node scripts/lessons.mjs edit --id my-lesson --patch '{"trigger": "git stash"}'

If a lesson has commandPatterns: ["\\bgit\\s+stash\\b"] and commandMatchTarget: "full" (default), this edit command would trigger the lesson even though no git stash is running.

Fix: set commandMatchTarget: "executable" — this strips everything after the first quoted argument before pattern matching.

Use "executable" for all guard lessons and any hint that targets a specific command name (not a flag or argument value).

Capturing lessons: what makes a good lesson

Emit #lesson tags in-session when you discover a problem→solution sequence:

#lesson
tool: Bash
trigger: osascript
problem: Using `osascript << EOF` (double-quoted) causes bash to expand $VAR references before osascript receives the script, silently corrupting AppleScript/JXA code.
solution: Use single-quoted delimiter: `osascript <<'EOF' ... EOF`. Escape literal $ as \$ if you need mixed expansion.
tags: tool:osascript, topic:heredoc, severity:silent-failure
#/lesson

Good lesson criteria:

  • Problem is specific and falsifiable. "This can cause issues" is not a problem. "Bash expands $VARS inside unquoted heredoc, corrupting the osascript input" is.
  • Solution is actionable. The reader should be able to apply it immediately without research. Include the exact command or pattern change.
  • Problem and solution are different. The solution must add information beyond re-describing the problem. lessons add blocks lessons where the two are ≥70% similar by token overlap.
  • Summary ≤80 chars, no trailing .... The summary is the injection header — it must fit on one line.
  • Solution ≥60 chars. Short solutions don't transfer knowledge. Expand with the "why" or an example.

Reviewing and promoting candidates

/lessons:review

Walks through pending candidates one at a time, grouped by tag, with a suggested edit and a promote/skip/edit decision for each. This is the recommended way to review a batch of candidates.

CLI equivalent

node scripts/lessons.mjs review # list candidates grouped by tag
node scripts/lessons.mjs promote --ids <id1>,<id2> # promote to active
node scripts/lessons.mjs promote --ids <id1> --archive "<id2>:reason" # archive bad ones

When reviewing, ask:

  1. Is the problem clearly falsifiable (not vague)?
  2. Does the solution give concrete, actionable guidance?
  3. Is the trigger narrow enough to avoid false positives?
  4. Is the trigger broad enough to catch all variants (not over-specified)?
  5. Would a future session recognize this as relevant without the original context?

Tuning existing lessons

/lessons:doctor

Audits all active lessons for the quality issues below and offers to fix them. To edit a field directly:

/lessons:manage → "edit priority on <slug> to <value>"

CLI equivalent

node scripts/lessons.mjs doctor # audit all active lessons
node scripts/lessons.mjs edit --id <slug> --patch '{"field":"value"}' # fix inline

Common tuning moves:

Doctor warningTypical fix
summary too longShorten to ≤80 chars; move detail to problem
no patterns — fires on every callAdd commandPatterns or pathPatterns
solution restates problemRewrite solution to only contain the fix, not the re-statement
overspecified triggerGeneralize the regex to the hazardous argument, not the full invocation
context bleedRemove "this repo", "last session", first-person references
solution-stalenessRemove or generalize version strings
directive with toolNamesRemove tool field or change type to hint

Priority tuning: if doctor reports priority homogeneity, differentiate high-impact lessons to priority 7-9 and routine hints to 3-5. Injection order within a priority cluster is arbitrary.