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
│ keep it fresh │── flag deleted refs · drift-triage · regenerate index
│ + guard the push │── secret scan blocks a leak before it's published
└─────────────────────┘
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.
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:
names that path surfaces right then — the gotcha shows up exactly when you touch the code.
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. 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. You can also trigger capture with /lore:capture.
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); --index regenerates
the store's README. /lore:stats prints a store-health snapshot (counts by status/category,
the drift backlog, long-unverified entries, dangling links). The optional pre-push hook runs
the existence check before every push.
| Command | What it does |
|---|---|
/lore:init |
Scaffold the store in this project; optionally install the pre-push hook. |
/lore:capture [note] |
Capture a learning from the current work (via the skill). |
/lore:lint [--report|--index|--strict] |
Freshness linter: ref-check, drift triage, index regen. |
/lore:stats |
Store-health snapshot: counts, drift backlog, stale-age, 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 is python3 ~/.codex/lore/verify_refs.py (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] # the linter checks these still exist
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.
Optional .lore.json in your project root:
{
"storeDir": "learnings",
"maxRecall": 5,
"staleStatuses": ["superseded", "obsolete", "deprecated"],
"secretAllow": ["\\bAKIAEXAMPLE\\b"]
}secretAllow is a list of regexes; a match on a line suppresses secret-scan
findings there (for genuine false positives or illustrative examples).
Learnings are committed and pushed by default so a whole team shares them. To keep
them local/personal instead, 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. - 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. - Hooks run code on every turn. Recall and capture execute local Python on each prompt
and 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: the end-of-turn nudge is the
Stophook inplugin/hooks/hooks.json. Remove that block to make capture purely manual (/lore:capture). - Different store location/size: use
.lore.json(above). - Recall tuning: the stop-word list and scoring live in
plugin/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.