vault-health — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited vault-health (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Run a comprehensive health check on the Obsidian vault: orphans, broken links, missing sections, stale notes, and growth statistics.
Obsidian must be open; obsidian CLI on PATH. Config at ~/.mnemo/config.json (required fields: vault, taxonomy, links_section) — schema in references/config-schema.md.
Steps 1-4 run in parallel — single assistant message batching the independent CLI reads (orphans, unresolved, tag counts; Step 4 reuses Step 3's tag output, no extra call). These are independent queries against the same indexed vault, ~180ms total vs ~720ms sequential.
Surface two common gotchas if claude-mem plugin is installed and enabled. If cascade.claude_mem.enabled is false, skip this section silently — many users intentionally disable claude-mem for CPU/RAM reasons.
bash "${CLAUDE_PLUGIN_ROOT}/scripts/check-cm-version.sh"
# Or from source: plugins/mnemo/scripts/check-cm-version.shScript emits three lines: version: X, stale: N, path: .... Interpret:
stale > 0 → warn: "claude-mem has {stale} old version folder(s) cached. Restart all Claude windows — old Stop hooks point to a path that no longer exists."version < 12 → warn: "claude-mem v{version} is behind v12 — you're missing file-read gate, tier routing, and knowledge agents. Run /plugin update claude-mem."obsidian orphans vault="{vault}"List notes with zero backlinks. These are invisible in Graph View.
obsidian unresolved vault="{vault}"Show [[wikilinks]] pointing to non-existent files. Ghost notes are NORMAL (entity discovery) — don't flag on raw count alone.
Actionable — top unresolved targets = missing hub notes (via obsidian eval, authoritative; CLI unresolved can lag/lie — see references/gotchas.md):
obsidian eval code="(()=>{const u=app.metadataCache.unresolvedLinks;const f={};Object.values(u).forEach(l=>Object.keys(l).forEach(t=>f[t]=(f[t]||0)+1));return JSON.stringify(Object.entries(f).sort((a,b)=>b[1]-a[1]).slice(0,10));})()" vault="{vault}"A short name with many refs (e.g. [[Diadoc]] ×30) = create a hub note Diadoc.md → [[MOC — …]] so all those links resolve (alias doesn't work for bare links — by design).
obsidian tags counts sort=count vault="{vault}"Show top 15 tags. Flag tags used only once (potential typos).
Reuse the `obsidian tags counts` output from Step 3 — do not call it again (the tag index is one query; Steps 3 and 4 are two views of the same result). From it, extract counts for taxonomy tags: #atom, #molecule, #source, #session, #moc. These correspond to config.taxonomy.*.tag values.
Total notes count:
obsidian files ext=md vault="{vault}" totalDo NOT loop `obsidian read` per file — on a 1000-note vault that's ~180s. Use a single filesystem grep against the vault directory.
VAULT_PATH=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-vault-path.sh" "{vault}")
# Single recursive grep -L: files NOT containing the links section heading.
# Filter to taxonomy-prefixed notes.
grep -rL --include="*.md" "{links_section}" "$VAULT_PATH" 2>/dev/null \
| grep -E "(Atom|Molecule|Source|Session|MOC) — "Measured on 999-note vault: ~49ms vs ~180s serial — 3600x speedup. Safe to run always.
Report notes missing the section.
# in names → permanent orphans)VAULT_PATH=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-vault-path.sh" "{vault}")
find "$VAULT_PATH" -name "*#*.md" -not -path "*/.obsidian/*" -not -path "*/.trash/*" 2>/dev/null | sed "s|$VAULT_PATH/||"Files with # in the name are permanent orphans — [[Note #1]] parses as [[Note]] + heading anchor #1, so nothing resolves to them (even existing links). Flag for rename (# → — or drop the #). Same for . mid-name (breaks CLI create). See references/tool-routing.md (naming rules).
A temporal signal, distinct from orphans (Step 1, which is structural): notes untouched longer than the threshold for their type are candidates for a re-read. Threshold precedence: per-note ttl: <days> → review.staleDays.<type> → review.staleDays.default → 30 (legacy). Age is measured from the newest of date or reviewed — so stamping reviewed: {today} on a still-valid note resets its clock. That snooze is what stops a stale list from rotting into guilt-debt (the canonical failure mode of review dates — see Gotchas).
VAULT_PATH=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-vault-path.sh" "{vault}")
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/review-candidates.py" "$VAULT_PATH" --limit 30
# Or from source: plugins/mnemo/scripts/review-candidates.pyOutput: CANDIDATES\t{n}, then THRESHOLDS\t{json}, then one tab-separated row per note ({overdue_days} {type} {anchor_date} {anchor_src} {threshold_days} {relpath}), most-overdue first. {threshold_days} is the budget actually applied to that note (per-note ttl: if set, else its type's staleDays, else default) — show it as (type, Nd budget), never invent a ttl. Pure filesystem — independent of the obsidian CLI graph cache (no lag/lie risk). A missing review config section reproduces the legacy uniform 30-day behavior, so this is safe before any config migration.
Don't AND this with backlinks — a well-linked note can still hold outdated claims. Report candidates on their own; cross-reference Step 1 yourself if you specifically want "old and orphaned."
review.lint.enabled)Steps 1-7 are cheap and structural. This is the content pass — Karpathy's "lint": actually re-read the notes to judge whether claims have rotted, instead of trusting the calendar. Together the checks cover his four: orphans (Step 1) + concepts-mentioned-but-no-page (Step 2 unresolved links) + stale claims (Step 7) + contradictions (here). Gated by config.json → review.lint.enabled (default false) because it reads note bodies and costs tokens — skip this step silently when disabled.
When enabled, take the top review.lint.maxCandidates (default 15) candidates from Step 7 and have them read & judged on the model set by config.json → review.lint.model (default `haiku`; sonnet/opus for higher-quality verdicts). This health skill itself runs as a haiku fork and cannot upgrade its own model, so:
review.lint.model is haiku (or unset) → do the lint inline in this fork.model: {review.lint.model} (Claude Code: Task tool, subagent_type: Explore or general; Codex: spawn_agent) that reads the candidate note bodies in one batched pass (filesystem read, not one obsidian read per file) and returns the verdicts. Keeping the cheap Steps 1-7 on haiku while the lint runs on opus is the whole point of the split.13 still-valid, 2 update-needed, NOT 15 still-valid. Defaulting the count to the candidate total (as if everything passed) is a reporting bug — wait for the subagent's verdicts before writing the report.Emit a verdict per candidate:
reviewed: {today} into the note's frontmatter via mcp__obsidian__str_replace — anchor on the frontmatter line inside the leading --- block (not a date:/reviewed: mention in the body): replace the existing reviewed: value, or if absent insert after date: (date: {d} → date: {d}\nreviewed: {today}). A confirmed-valid note then stops resurfacing without a manual edit. This auto-stamp is on by default (config.json → review.lint.autoStampReviewed, default true); only if the user set it to `false` do you just recommend the stamp and write nothing. When the lint runs in a spawned subagent (model ≠ haiku), that subagent does the stamping — it already holds the verdict and the note path. If a stamp write fails (Obsidian offline, or mcp__obsidian__str_replace unavailable in the subagent context), don't drop it silently — collect those note paths and surface them under the Content lint report block so the user can stamp them manually.Verdicts are triage, not truth — on haiku especially, expect false positives; even on opus, surface them as questions. The only write health ever performs is the reviewed: auto-stamp above, and only ever on a still-valid verdict — never content, never on update-needed/contradicts. It fires only inside this lint pass (which is itself off unless review.lint.enabled is true), and can be turned back to suggest-only with autoStampReviewed: false. The user stays in control.
Enumerate the MOC notes (by the moc taxonomy prefix), then count backlinks for each:
VAULT_PATH=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-vault-path.sh" "{vault}")
find "$VAULT_PATH" -name "{moc_prefix}*.md" -not -path "*/.obsidian/*" -not -path "*/.trash/*" 2>/dev/null | sed "s|.*/||;s|\.md$||" # MOC namesFor each enumerated MOC name, count backlinks:
obsidian backlinks file="{moc_name}" vault="{vault}"Sort by count, show top 5. Keep the enumerated MOC-name list — Step 8.5 reuses it to find topics that have no MOC.
Karpathy's lint also proposes what to research next. Steps 1-8 already collected the raw signal — turn it into growth suggestions without any new CLI calls (reuse Step 2's obsidian eval top-10 unresolved targets, Step 3/4's tag counts, and the MOC-name list enumerated in Step 8). Two cheap, computable gap types:
config-schema.md uses for creating a MOC) whose {moc_prefix}{Topic} is absent from Step 8's enumerated MOC-name list. Suggest: "12 notes tagged #auth, no MOC → create MOC — Auth?"unresolved output) cited ≥5 times that reads like an external tool/paper/vendor (not a short project name, which instead wants a hub note) and has no Source — … note. Suggest: "[[LangGraph]] cited ×9, no Source note → capture one?"These are suggestions, never auto-created (same non-destructive stance as the rest of health). Skip a type silently when it yields nothing. Do not web-search to fill the gap — mnemo maintains a human-authored vault: it points at the gap, the user decides whether to fill it. (This is the on-philosophy half of Karpathy's "suggest new article candidates"; the auto-web-imputation half is deliberately omitted.)
📊 Vault Health Report ({date})
⚠️ claude-mem: {warning or "v12.3.9, clean"}
Total: {N} notes
Atoms: {N} | Molecules: {N} | Sources: {N}
Sessions: {N} | MOCs: {N} | Other: {N}
🔴 Orphans: {N}
- Note Name 1
- Note Name 2
🟡 Missing {links_section}: {N}
- Note Name 1
🚫 Bad filenames (`#`/`.`): {N} — permanent broken links, rename
- Atom — Foo (PR #12) → rename to "PR 12"
🔍 Top unresolved targets (missing hub notes?):
1. [[Diadoc]] ×34 → create hub note?
2. [[Python]] ×28
🔗 Unresolved wikilinks: {N} total
📏 Tags: {N} total, {N} used once
🏆 Top-5 Hubs (most backlinks):
1. MOC — Security (34)
2. MOC — AI ML Tools (28)
...
💤 Review candidates (stale by type-aware age): {N}
- Atom — API X gotcha — 45d overdue (atom, 60d budget)
- Source — vendor API pricing — 35d overdue (source, 180d budget)
(snooze a still-valid note: add `reviewed: {today}` to its frontmatter)
🔬 Content lint: {N judged} — {S} still-valid, {U} update-needed, {C} contradicts ← only when review.lint.enabled
(counts MUST equal the lint's actual verdicts — never default to all-still-valid; see Step 7.5)
- Atom — API X gotcha → UPDATE-NEEDED: superseded by [[Atom — API X v2]]
- Source — vendor API pricing → still-valid (stamped reviewed: {today})
🌱 Research-gap candidates (where the vault wants to grow): {N}
- #auth ×12 notes, no MOC → create MOC — Auth?
- [[LangGraph]] ×9, no Source note → capture one?
🧠 Claude memory/ index: {KB}KB / {lines} lines {✅ lean | ⚠️ bloated → autodream}Omit the 🔬 Content lint block entirely when review.lint.enabled is false. Omit the 🌱 Research-gap candidates block when Step 8.5 found nothing. The still-valid line above shows the default (autoStampReviewed: true — the note was stamped); with autoStampReviewed: false render it as → still-valid (recommend reviewed: {today}) instead, since nothing was written.
Skip the ⚠️ claude-mem line entirely if Step 0 found nothing to warn about.
Separate from the Obsidian vault, Claude Code keeps an always-loaded index at memory/MEMORY.md. Claude Code auto-memory hard-truncates this index at ~24.4 KB on load — beyond that, trailing rows silently vanish from Claude's context. So warn early (before the cliff), not at some lax size. Threshold is configurable via config.json → memory.indexWarnKB (default 22):
WARN=$(python3 -c "import json,os;print(json.load(open(os.path.expanduser('~/.mnemo/config.json'))).get('memory',{}).get('indexWarnKB',22))" 2>/dev/null || echo 22)
for f in "$HOME"/.claude/projects/-*/memory/MEMORY.md; do
[ -f "$f" ] || continue
kb=$(( $(wc -c < "$f") / 1024 )); ln=$(wc -l < "$f")
[ "$kb" -gt "$WARN" ] && echo "⚠️ $(basename "$(dirname "$(dirname "$f")")"): ${kb}KB / ${ln} lines — >${WARN}KB warn (auto-memory truncates ~24.4KB) → run autodream (move sessions → MEMORY-archive-index.md, target <20KB)"
doneIf flagged → recommend autodream (memory consolidation): slim the index into topic files + MEMORY-archive-index.md, no loss. Procedure: ~/.claude/memory/autodream-principles.md. This is the only memory/ check here — vault-health otherwise audits Obsidian, not Claude's memory/.
Common failures in references/gotchas.md. Skill-specific rules:
obsidian orphans may return empty on small vaults — this is OK, not an error.orphans/unresolved/backlinks lag writes and have shown a note as resolved AND broken at once. For critical checks use obsidian eval on metadataCache (see references/gotchas.md). Treat counts as advisory if notes were created this session.review.lint.autoStampReviewed (default true): it lets the content-lint stamp reviewed: {today} on a still-valid note (Step 7.5) to close the snooze loop. That is the sole frontmatter write health can make — only the reviewed: field of a confirmed-valid note, never content, never anything else. It fires only when the content lint itself is enabled (review.lint.enabled, default false), so a default install still writes nothing; set autoStampReviewed: false to keep the lint suggest-only.haiku especially, contradiction/update calls have false positives. Surface them as questions, never as facts, and never act on them automatically. The whole point of reviewed: is to let a quick human confirm and silence a false flag.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.