A self-maintaining lore system for Claude Code and OpenAI Codex: capture the non-obvious things you learn while working, automatically surface the relevant ones back into context next time, and keep them from going stale as the code changes. Each task makes the next one easier.
Every unit of work should leave behind something that makes the next unit of work cheaper. This plugin is that flywheel for a codebase.
A hooks plugin is invisible by design — here's the part you'd otherwise never see: recall appears on its own (you ran no command), and the LORE CHECK nudge closes the turn so capture never silently gets skipped.
The terminal output is real — produced by the actual recall.py /
capture_check.py hooks against the shipped example learning; only the typing
pace is scripted.
you prompt
│
▼
┌─────────────────────┐ UserPromptSubmit hook
│ recall │── surfaces relevant past learnings into context
└─────────────────────┘
│
▼
the agent works, using them
│
▼
┌─────────────────────┐ Stop hook
│ capture nudge │── "did this turn produce a durable learning?"
└─────────────────────┘
│ yes
▼
┌─────────────────────┐ lore skill
│ write one entry │── a single markdown file under learnings/
└─────────────────────┘
│
▼
┌─────────────────────┐ linter + pre-push hook + CI
│ keep it fresh │── flag deleted refs · drift-triage · regenerate index
│ + guard the push │── secret scan blocks a leak before it's published
└─────────────────────┘
Starting from empty? /lore:mine reads your git history — reverts, fix
commits, the files with the most churn, the messages that explain a why — and
proposes the first entries, so the loop has something to recall on day one
instead of week six.
The store is plain markdown in your repo (committed, so your team benefits). The behavior lives in the plugin. Nothing is sent anywhere — recall is a local text match. Because the store is pushed by default, treat it as published: see Security.
- Claude Code or OpenAI Codex (CLI or IDE extension).
- Python 3 on
PATHaspython3,python, orpy(stdlib only — nopip install). - On Windows with Claude Code, the bundled Git Bash runs the hook wrappers (already required by Claude Code). The Codex installer bakes the interpreter path, so it needs no wrapper.
/plugin marketplace add aoc81/lore
/plugin install lore@lore
Then, once per project: /lore:init — creates the learnings/ store (seeded with a
template + example) and optionally installs the non-blocking pre-push freshness hook.
From a clone of this repo — install once per machine, then trust the hooks in Codex:
python3 codex/install.py # registers recall + capture hooks under ~/.codex
# open Codex → /hooks → trust the two hooks
python3 codex/install.py --store # per project: scaffold ./learnings (optional)
Codex uses the same UserPromptSubmit / Stop + additionalContext contract, so recall
and capture work natively and the core scripts + store are reused unchanged. Details and
caveats: codex/README.md.
Recall (read). On every prompt, a UserPromptSubmit hook tokenizes what you typed,
matches it against each learning's title + tags (never the body), and injects the
top few matches as paths + titles. The agent reads a file only if it's actually relevant.
Matching is word-boundary, not substring — "auth" never matches "author" — with an
entry-level score threshold so a single weak overlap never injects noise; short words
(ci, api, aws) match against exact tags, where they're curated vocabulary.
Tokenizing is Unicode and accent-folding, so a prompt in Spanish (or any accented
language) tokenizes correctly and "autenticación" matches a tag written autenticacion;
common English and Spanish filler words are ignored, and you can add your own with
stopWords in .lore.json.
Superseded entries are down-ranked and tagged so they're never mistaken for live guidance,
and each match carries cheap freshness flags (a deleted-file ref, or "verified N months
ago") so a possibly-stale learning is never trusted blindly. A PreToolUse hook adds
edit-time recall: when the agent is about to edit a file, any learning whose files:
covers that path — exact, a directory prefix (src/auth/), or a glob (src/auth/*.py) —
surfaces right then, so the gotcha shows up exactly when you touch the code (live entries
ahead of superseded ones, and each entry only once per session, so a ten-edit refactor
doesn't re-inject the same context ten times). Every
surfaced entry is also logged to .git/lore-recall.log (local only, inside .git,
never committed) — and a PostToolUse hook logs it again when the agent actually
reads the file, so /lore:stats can separate shown from used: an entry
surfaced 40 times and never opened has a misleading title, not a useful lesson.
Recall searches two stores when you configure one: the committed team store and
a private personalStoreDir (see Two stores).
You can also query the same scorer by hand with /lore:search <query>.
Capture (write). A Stop hook reminds the agent, at the end of a turn, to record a
learning only if it's both non-obvious and reusable. By default the nudge is smart:
purely conversational turns (no tool use at all) are skipped, so the check keeps its
signal instead of becoming a mechanical reflex — configurable via captureNudge in
.lore.json ("always", "smart", "off"). The lore skill defines the gate,
the routing, and the file format. Before writing, it runs a deterministic overlap check
(recall.py --query) so a new learning updates a related entry instead of duplicating
it, and after writing it runs the secret scanner on the new file — a leak is caught at
write time, with the pre-push hook as backstop. A SessionStart hook re-injects a short
lore reminder right after context compaction — exactly the moment an uncaptured
learning would otherwise die with the context. You can also trigger capture with
/lore:capture.
Cold start (write, in bulk). An empty store surfaces nothing, so the plugin
looks dead and nobody captures — the failure mode that kills this idea before it
pays off. /lore:mine breaks it: it scores your git history for reverts, fix
commits, long explanatory messages, and churn hot spots, marks any candidate an
existing entry already covers, and hands the agent a ranked worklist to write
real entries from — under the same gate as any other capture, evidence read from
git show first.
Freshness. Code changes; learnings shouldn't silently rot. /lore:lint checks
that each entry's files: still exist; --report ranks entries whose referenced code
changed since they were last verified: (your re-verify worklist — computed in a single
streaming git log pass, not one subprocess per file); --dupes finds near-duplicate
entry pairs (e.g. two teammates capturing the same gotcha on parallel branches) and
category-name variants; --index regenerates the store's README. /lore:stats prints a
store-health snapshot (counts by status/category, the drift backlog, long-unverified
entries, recall activity — including entries that never surface and entries that
surface but are never read — near-duplicate count, dangling links). The optional
pre-push hook runs the existence check before every push, and the linter warns when the
.lore/ hook copies fall behind the installed plugin version — or when .lore.json
has a key the hooks are silently ignoring (a typo like maxRecal).
For a team, /lore:init also offers a CI guard — a small GitHub Actions workflow
running the same two scripts on every PR, so the secret scan protects everyone, not
just whoever installed the git hook (see Security).
| Command | What it does |
|---|---|
/lore:init |
Scaffold the store in this project; optionally install the pre-push hook and the CI guard. |
/lore:mine [--since …] |
Bootstrap from git history — propose learnings from past fixes, reverts, and churn. |
/lore:capture [note] |
Capture a learning from the current work (via the skill). |
/lore:search <query> |
Search the store by title/tags — the same scorer the recall hook uses. |
/lore:lint [--report|--dupes|--index|--strict] |
Freshness linter: ref-check, drift triage, dupe triage, index regen. |
/lore:stats |
Store-health snapshot: counts, drift backlog, recall activity, dupes, dangling links. |
/lore:sweep [scope] |
Semantically re-verify drifted entries against the code and update them. |
/lore:scan [path] |
Scan the store for committed secrets (the pre-push guard, run on demand). |
These are Claude Code slash commands. On Codex there are no /lore:* commands —
init is python3 codex/install.py, capture is the lore skill + the Stop hook,
and lint/mine are direct python3 ~/.codex/lore/{verify_refs,mine_history}.py
calls (see codex/README.md).
One fact per file, learnings/<category>/<slug>.md, with frontmatter:
---
title: "Short, specific — this is what recall matches on"
date: 2026-01-01
track: knowledge # or: bug
category: ci
tags: [ci, cache, lockfile]
files: [.github/workflows/ci.yml] # exact path, dir prefix (src/auth/), or glob (src/*.py)
status: current # or: superseded / obsolete
verified: 2026-01-01 # optional; bump after a re-verify to clear it from drift triage
---Body — bug: Problem · Root Cause · What Didn't Work · Solution · Prevention. Body — knowledge: Context · Guidance · Why This Matters · When To Apply.
Authoring for low drift: reference code by stable symbol (function/class), not line
numbers; keep files: complete (it's the linter's surface); make tags the words a
future prompt would use — in the language you actually prompt in (recall folds
accents, so autenticación and autenticacion match either way, but a Spanish prompt
still won't match an English tag).
Optional .lore.json in your project root:
{
"storeDir": "learnings",
"maxRecall": 5,
"staleStatuses": ["superseded", "obsolete", "deprecated"],
"staleAfterMonths": 6,
"stopWords": ["widget", "todo"],
"secretAllow": ["\\bAKIAEXAMPLE\\b"],
"captureNudge": "smart",
"personalStoreDir": "~/.lore/learnings"
}staleStatuses are the status: values treated as not-live: they're down-ranked in
prompt recall, sorted last in edit-time recall, tagged [SUPERSEDED] when surfaced,
and exempt from the linter's actionable issues.
staleAfterMonths is the single freshness threshold, used both by recall's
"verified N months ago" flag and by the linter's long-unverified count.
stopWords are added to the built-in stop list (English + Spanish fillers) — use it
for words that are noise in your domain, e.g. a product name that appears in every prompt.
secretAllow is a list of regexes; a match on a line suppresses secret-scan
findings there (for genuine false positives or illustrative examples).
personalStoreDir enables the private second store (""/absent = off). It may be
absolute, ~-prefixed, or project-relative; a path inside the team store is
refused, since it would be committed anyway. See
Two stores.
captureNudge controls the end-of-turn capture nudge: "smart" (default —
skip turns that used no tools at all), "always" (nudge every turn), or
"off" (capture is manual via /lore:capture). Booleans work as shorthand:
true = "smart", false = "off". Wrongly-typed config values are
ignored rather than crashing a hook — run /lore:lint to see which keys are
being ignored and why.
Learnings are committed and pushed by default so a whole team shares them. But not every learning is about the codebase — the capture gate also catches how you want to be worked with ("show me the diff before committing", "don't add comments I didn't ask for"), and that does not belong in a teammate's PR.
So there are two stores, split by who else has to read it:
storeDir (team) |
personalStoreDir (personal) |
|
|---|---|---|
| Contents | the codebase: bugs, gotchas, decisions | you: preferences, working conventions |
| Committed | yes — that's the point | no (outside the repo, or gitignored) |
| Recalled | yes | yes — same scorer, flagged [personal] |
| Indexed / linted / secret-scanned | yes | no — nothing is ever published |
Turn it on with one key (the skill then routes preference-shaped learnings there instead of the shared store):
{ "personalStoreDir": "~/.lore/learnings" }/lore:stats counts personal entries so a two-store setup never looks half-empty.
To make the whole store private instead, just add your store directory (e.g.
learnings/) to .gitignore.
The store is committed and pushed by default and capture is autonomous — so a learning is effectively published the moment it's written. The plugin is built around that:
- Don't write secrets. The capture skill is instructed to reference secrets, never quote them (no keys, tokens, credentials, connection strings, or PII in an entry).
- Blocking secret scan at the push boundary. The pre-push hook runs
scan_secrets.pyover the store and aborts the push if it finds a likely key/token/credential. Run it any time with/lore:scan. Bypass a false positive withlore:allow-secreton the line, asecretAllowregex,LORE_SCAN_BLOCK=0, orgit push --no-verify. - The same scan in CI, for the whole team. A git hook only protects the machine
it's installed on, so one teammate who skipped
/lore:initcan still push a secret./lore:initoffers a ~15-line.github/workflows/lore.ymlthat runspython .lore/scan_secrets.pyandpython .lore/verify_refs.py --stricton every PR — from the.lore/copies already committed to the repo, so CI and the local hook can't drift apart, and nobody else installs anything. On Codex:python3 codex/install.py --ci. If you'd rather wire it by hand, the file is plugin/templates/lore-ci.yml. - Recall never echoes bodies. The recall hook matches only
title+tagsand emits only paths + titles — store content is never auto-injected verbatim, which limits the prompt-injection surface of a shared/poisoned store. The titles it does inject are capped at 140 characters and stripped of control characters, so atitle:in a teammate's PR can't smuggle in an instruction payload either. - Hooks run code on every turn. Recall and capture execute local Python on each
prompt, each file edit, each file read (the usage-telemetry hook, which exits
immediately unless the path is inside a store), and at turn-end. Review the scripts
before trusting them; on Codex, approve them via
/hooks. - Pin the plugin. Install a reviewed tag/commit rather than floating on
main, so you control when new hook code starts running.
Everything runs locally with no network calls; the only thing that ever leaves your machine is what you push — which is exactly what the secret scan guards.
- Quieter capture: set
"captureNudge": "off"in.lore.json(or"always"to restore the pre-0.3 every-turn nudge). This is per-project and survives plugin updates — don't edit the plugin's ownhooks.json, that directory is an ephemeral cache. - Keep personal preferences out of the team store: set
personalStoreDir(Two stores). - Different store location/size: use
.lore.json(above). - Recall tuning: add noise words with
stopWordsin.lore.json(per-project, survives plugin updates); the built-in list and the scoring itself live inplugin/scripts/recall.py.
Everything is stdlib Python, so one setup works on macOS, Linux, and Windows. Hooks use
the stdin-JSON / additionalContext contract that Claude Code and Codex share — the
Claude target resolves python3/python/py via a tiny sh wrapper; the Codex
installer bakes the interpreter path. No runtime dependencies, no network calls.
MIT — see LICENSE.