Disclaimer: This is an independent third-party tool. It is not affiliated with, endorsed by, or sponsored by Anthropic. "Claude" and "Claude Code" are trademarks of Anthropic and are used here nominatively to identify the official CLI/product this tool integrates with.
A static lint for Claude Code memory directories. Catches the file-quality problems that lead to context pollution before they reach the prompt — stop generic-keyword noise at the source instead of chasing it at runtime.
cml check ~/.claude/projects/<id>/memory/ # exits 1 on ERROR
cml fix ~/.claude/projects/<id>/memory/ # auto-add missing aliases
cml stats ~/.claude/projects/<id>/memory/ # counts only, no contents
Claude Code memory directories grow quickly. Once frontmatter is
inconsistent and a meaningful fraction of files only carry generic
keywords (api, github, claude, task, agent, …), runtime
routing degrades into "every common word matches every file": context
gets stuffed with unrelated memory and the model's attention drifts.
Runtime routers can patch the symptom. The cause is on the write
side: missing frontmatter, empty aliases:, oversized files, stale
copies. claude-memory-lint raises those defects to ERROR / WARN /
INFO at write-time so they never reach the auto-load path.
This is the privacy-conscious choice in the memory-tooling space:
- No LLM calls. Period. Heuristics only.
- No file body in stdout.
statsandcheck --format jsonprint filenames, rule IDs, and counts — never the file content. - Auto-fix writes a
.baknext to the original and never sends anything off-machine. - A separate hook (
claude-memory-router) handles runtime routing with the same posture; this lint is its compile-time companion.
pip install claude-memory-lint
# or for development:
pip install -e .[dev]Python 3.10+. No required runtime dependencies.
| ID | Severity | What it catches |
|---|---|---|
| R001 | ERROR | frontmatter missing or unterminated |
| R002 | ERROR | both aliases: and triggers: are empty / absent |
| R003 | WARN at 25 KiB / ERROR at 30 KiB | file size threshold (lowered from 50 KiB in v0.1.2) |
| R004 | WARN | filename stem is not kebab-case ASCII |
| R005 | INFO | inbound: file is not referenced by any other memory file (orphan) |
| R006 | WARN | stop-word density in name + description ≥ 40 % |
| R007 | INFO | duplicate normalised stem (likely stale copy) |
| R008 | INFO | mtime older than 180 days |
| R009 | ERROR | secret literal pattern (GitHub PAT / AWS / Anthropic / OpenAI / Google / Slack / Stripe) — match is never echoed in the lint output |
| R010 | WARN (opt-in) | outbound: dangling markdown link ](*.md) — target not found in tree or archive/ (enable via --dangling-links). Complementary to R005: R005 catches a file no one points at; R010 catches a link that points at nothing |
| R011 | WARN (opt-in) | stale backup file *.bak / .backup / .orig / trailing ~ older than 7 days in the active directory (enable via --stale-backup) |
| R012 | WARN (opt-in) | frontmatter aliases: / triggers: items that reduce to stop-words only (re-introduces the 1-hit pollution R006 removes from name+description); enable via --trigger-stopwords |
| R013 | WARN (opt-in) | high-salience emphasis emoji (🔥 🚨 ⚠ 🛑 ❌ ✅ 💥 ❗) over threshold — uniform priority signalling collapses LLM attention weighting; enable via --emphasis-density (threshold via emphasis_max, default 5) |
| R014 | WARN (default ON) | frontmatter supersedes: target does not resolve in tree or archive/ — R010 lifted to a structured field; disable via --no-supersedes-check |
| R015 | WARN at 4 KiB / ERROR at 8 KiB (default ON) | auto-inject file body exceeds per-turn token budget — detected via frontmatter inject:/auto_load: or filename pattern (MEMORY.md, *-core.md); R003's 25/30 KiB cold-file threshold does not catch per-turn repeating cost; disable via --no-inject-bloat-check |
| R016 | WARN (default ON) | INDEX-role file (MEMORY.md, INDEX.md, or frontmatter role: index) has more than 200 lines ([lines]) or body content mixed into what should be an entry-list only ([body-mixed]); disable via --no-index-purity-check |
| R017 | WARN (opt-in) | memory body line promises an artifact at an absolute path (~- or /-rooted) that does not exist on disk — operationalises the "implemented-in-prose-only" pattern; enable via --phantom-artifact |
Thresholds are tunable in code; runtime config file support is on the roadmap.
R009 (added in v0.2.0) is the response to a real-world incident where a
GitHub PAT literal sat in a memory file for days under .gitignore
"protection" while still being one filesystem-read away from the LLM
context. The rule deliberately reports only the pattern type and
the line — the matched substring is never written to stdout, JSON,
SARIF, or any error path — because a lint that leaks the secret it
caught is worse than no lint.
Add to your .pre-commit-config.yaml:
repos:
- repo: https://github.com/hinanohart/claude-memory-lint
rev: v0.4.0
hooks:
- id: claude-memory-lint
args: [check, --rule, R001, --rule, R002]This blocks commits that introduce a memory file without proper frontmatter or aliases.
- name: lint memory directory
run: |
pip install claude-memory-lint
cml check --format sarif path/to/memory > cml.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: cml.sarifThis lint pairs with claude-memory-router:
- The lint enforces write-time quality (aliases, size, freshness).
- The router consumes that quality at read-time and only injects the most relevant memory files into the prompt.
Garbage-in, garbage-out is real for routers as for any other engine. The lint exists so the router can do its best work.
text default — human-readable, one finding per line + summary
json machine-readable; convenient for CI gates
sarif SARIF 2.1.0 — uploadable to GitHub code scanning
pip install -e .[dev]
pytest -vThe current suite is 95 tests covering parser edge cases (no
frontmatter / unterminated frontmatter / inline lists / multi-line
lists), per-rule positive and negative cases for R001-R017, the
corpus rules R007 and R011, and the CLI front-end including JSON /
SARIF reporters.
MIT. See LICENSE.