Skip to content

Repository files navigation

Lore

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.

See it in action

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.

lore in action — recall surfaces a relevant past learning automatically, then the LORE CHECK capture nudge fires at the end of the turn

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.

The loop

        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.

Requirements

  • Claude Code or OpenAI Codex (CLI or IDE extension).
  • Python 3 on PATH as python3, python, or py (stdlib only — no pip 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.

Install

Claude Code

/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.

OpenAI Codex

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.

How it works

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).

Commands

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).

The learnings format

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).

Configuration

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.

Two stores: team and personal

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.

Security

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.py over 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 with lore:allow-secret on the line, a secretAllow regex, LORE_SCAN_BLOCK=0, or git 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:init can still push a secret. /lore:init offers a ~15-line .github/workflows/lore.yml that runs python .lore/scan_secrets.py and python .lore/verify_refs.py --strict on 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 + tags and 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 a title: 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.

Customizing

  • 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 own hooks.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 stopWords in .lore.json (per-project, survives plugin updates); the built-in list and the scoring itself live in plugin/scripts/recall.py.

How it's built

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.

License

MIT — see LICENSE.

About

Capture non-obvious learnings while coding; auto-recall the relevant ones next time. Plugin for Claude Code & Codex.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages