π§ claude-obsidian-kit
LLM wiki kit
First committed by selmakcby
Published in selmakcby/claude-obsidian-kit. Publisher page
26 GitHub stars
What it is
Give your Claude Code agents a shared, persistent brain β an Obsidian vault that survives sessions. Includes a retrofit guide for folders you already have, and a runnable vault linter.
AutoGineer overview, from the brainβs own files
claude-obsidian-kit transforms ephemeral Claude Code sessions into a persistent knowledge base. By introducing specialized agentsβthe archivist for reading and linting, and the scribe for writingβit enables agents to maintain context across sessions. The kit provides a structured vault template, a schema-driven constitution, and a linter to ensure the integrity of the knowledge graph, effectively turning a project's reasoning into a searchable, long-term asset.
How it works
Memory model: LLM wiki
AutoGineer overview, from the brainβs own files
The kit installs two specialized agents into your project: an archivist that handles queries and vault health, and a scribe that performs efficient writes. It uses a structured Obsidian vault layout with predefined folders for architecture, decisions, design, entities, and concepts. Agents follow a specific llm-wiki skill protocol for ingesting information, querying the vault, and linting for issues like broken links or orphans. A provided Python script allows for command-line linting of the vault's health.
| Folder | Role | Files |
|---|---|---|
| agents | agent-instructions | 2 |
| scripts | scripts | 2 |
| skills | agent-instructions | 4 |
| obsidian | folder | 3 |
| vault-template | folder | 10 |
Requirements
Databases: PostgreSQL
Set it up
This brain runs code automatically: 1 script. Review scripts/install.sh before opening the folder in Claude Code. No step below turns a hook on for you.
scripts/install.sh:sh scripts/install.sh
- Download the brain as a ZIP and unzip it where you keep your notes or project: https://mp-staging.autogineer.com/agent-brains/selmakcby/claude-obsidian-kit/download
cd claude-obsidian-kit && claude
AutoGineer scanned these files at commit 2826401.
- Not scanned by AutoGineerThe authorβs README also describes installing it from a clone of its repository. Read its install section: AutoGineer did not scan that path. https://github.com/selmakcby/claude-obsidian-kit/blob/282640137e4761c5a6b0aa75aa509cf39199d6c4/README.md#install
First committed by selmakcby. Published in selmakcby/claude-obsidian-kit. Licence: MIT License.
Usage
AutoGineer overview, from the brainβs own files
- Automating the ingestion of project decisions into a persistent vault.
- Querying past architectural decisions to inform current development.
- Running automated health checks on the knowledge base to identify stale notes or broken links.
- Maintaining a shared knowledge graph that survives individual agent sessions.
- βWhy did we choose Stripe webhooks for the payment integration?β
- The archivist agent reads the relevant decision note from the vault and synthesizes the rationale in seconds.
- βCheck vault health.β
- The archivist scans the vault for orphans, broken links, and contradictions, returning a severity-sorted report.
Documentation by selmakcby
<div align="center">
π§ claude-obsidian-kit
Give your Claude Code agents a shared, persistent brain.
Turn ephemeral sessions into accumulating knowledge β stored in an Obsidian vault. Built for efficiency: right model for each job, cheap writes, index-first reads.
ββ planner (opus) ββ
β ui-agent (sonnet)β QUERY / LINT
β builder (sonnet)β ββββββΆ archivist (sonnet Β· read-only)
β reviewer (sonnet)β β
ββββββββββββββββββββ βΌ
β VAULT
βββββββ INGEST βββΆ scribe (haiku Β· writes)
β²
β
Main Claude
(orchestrator)Install Β· Retrofit Β· Lint Β· Efficiency Β· FAQ Β· Prompts πΉπ·
</div>
What this is
A drop-in kit that extends Claude Code with:
Two agents:
archivistβ vault reader + health guardian (QUERY, LINT)scribeβ dedicated vault writer (INGEST, runs on haiku for efficiency)
One skill β
llm-wikiβ the INGEST Β· QUERY Β· LINT protocolsOne constitution β
vault-template/CLAUDE.mdβ the schema every agent reads before touching the vault (πΉπ· TΓΌrkΓ§e)One vault template β pre-structured Obsidian vault with conventions
One linter β
vault-lint.py, the LINT protocol as a runnable command (no dependencies)One retrofit guide β RETROFIT.md, for when you already have months of material
Obsidian config β graph colours by layer, and the
[!conflict]callout the skill needs
After install, your agents read from and write to a shared markdown knowledge graph that survives sessions. Yesterday's decisions are tomorrow's context.
The problem this solves
Default Claude Code agents are stateless:
Monday: agents plan a feature, build it β close session
Tuesday: new session β "why did we use Stripe webhooks?"
Claude: "I don't know. Let me re-analyze from scratch..."Every session starts from zero. Context dies with the conversation. The project's reasoning evaporates even when the code persists.
With this kit:
Monday: agents plan β scribe files to vault/decisions/
Tuesday: "why Stripe webhooks?"
archivist β reads vault/decisions/2026-04-21-stripe-webhooks.md β answers in 1 second with full rationaleInstall
Quick
git clone https://github.com/selmakcby/claude-obsidian-kit ~/claude-obsidian-kit
cd ~/claude-obsidian-kit
./scripts/install.sh ~/my-projectInstalls:
~/my-project/.claude/agents/archivist.md~/my-project/.claude/agents/scribe.md~/my-project/.claude/skills/llm-wiki/~/my-project/vault/(if doesn't exist)
Final step
Update your project's CLAUDE.md with vault permissions:
{
"permissions": {
"allow": [
"Write(vault/**)",
"Edit(vault/**)",
"Read(vault/**)"
]
}
}How it works
The 6-agent team
After this kit, your full team:
| Agent | Role | Model | Why this model |
|---|---|---|---|
| planner | Architecture + planning | opus | Deep reasoning |
| ui-agent | Design + components | sonnet | Balanced |
| builder | Backend implementation | sonnet | Coding quality |
| reviewer | Quality + security | sonnet | Thorough analysis |
| archivist | Vault reads (QUERY, LINT) | sonnet | Synthesis + audit |
| scribe | Vault writes (INGEST) | haiku | Cheap + fast |
The scribe / archivist split is token-efficient by design β see EFFICIENCY.md.
Three operations
π’ INGEST (scribe)
Other agents produce findings. Main Claude passes findings to scribe. Scribe formats into proper note + writes to correct folder + updates index.
planner: "We should use monthly/annual pricing toggle"
β
main Claude: Task(scribe, finding="...", type=decision)
β
scribe: writes vault/decisions/2026-04-23-pricing-toggle.mdπ΅ QUERY (archivist)
you: "Why did we choose monthly/annual toggle?"
β
archivist: reads index.md β greps "toggle" β reads 2-3 notes β synthesizes answerπ‘ LINT (archivist)
you: "Check vault health"
β
archivist: scans for orphans, broken links, stale notes, contradictions
archivist: returns severity-sorted report with fix suggestionsRetrofit β you already have a folder
vault-template/ assumes you are starting clean. Almost nobody is. The realistic situation is
months of accumulated material β transcripts, drafts, half-finished projects, and often an older
vault somewhere inside that already works.
RETROFIT.md is the guide for that case. It covers the two decisions you make before writing anything (the vault root is the folder you already have; an existing vault inside it is canonical and read-only), and the parallel-ingest design that keeps agents from writing the same entity page twice:
Phase 1 domain agents βββΆ project + decision pages only
(reference [[entities]], write none)
β
βΌ union + dedupe + deterministic split, in code
Phase 2 node agents βββΆ disjoint slices of the entity/concept list
β
βΌ
Phase 3 index + lint βββΆ one owner for index.md, one auditSplitting by page type rather than only by domain makes collisions impossible by construction rather than by instruction. Reference run: 1.9 GB, ~1,000 text files, 226 pages, 3,484 links, zero broken links, zero fabricated source citations.
Lint, as a command
skills/llm-wiki/lint.md specifies the checks and a health-score formula. vault-lint.py
implements that spec so the result is measured rather than asserted. Single file, standard
library only, read-only:
python3 scripts/vault-lint.py path/to/vault
# retrofit case: audit the wiki layer, resolve links against the whole folder
python3 scripts/vault-lint.py . --notes 'wiki/*' --notes 'index.md'
# non-English frontmatter
python3 scripts/vault-lint.py . --fields tur,durum,guncelleme --date-field guncelleme
# CI
python3 scripts/vault-lint.py . --fail-under 85| Check | Why it earns its place |
|---|---|
| Broken links | Code spans are stripped first β an [[example]] inside backticks is not a broken link |
| Orphans | Entry points (index, log, README) are exempt |
| Unverifiable sources | Every path a note cites in source: is checked against disk. This is how you catch a citation the agent invented |
| Ambiguous targets | Two files sharing a basename means [[name]] resolves by proximity β possibly not the file you meant |
| Missing concept pages | A link referenced 3+ times with no page behind it |
| Frontmatter, stale notes | Per the schema; stale = old date still in draft |
Flagged > [!conflict] callouts are reported but not scored. A marked contradiction is the
protocol working; the dangerous one is unmarked, and no linter can see that. Pass
--score-conflicts for the strict formula.
Obsidian config
The skill tells the agent to flag contradictions with > [!conflict] β and Obsidian has no such
callout type, so they render as anonymous grey notes. obsidian/ fixes
that and colours the graph by layer. Copy it into your vault's .obsidian/.
Efficiency
This kit is intentionally designed for low token cost. See EFFICIENCY.md for all 10 patterns.
Highlights:
Separate thinking from writing β opus plans, haiku files (80% cost reduction on writes)
Index-first reads β never grep the whole vault
Model tiering β haiku for scribe, sonnet for reads, opus for architecture
Least-privilege tools β smaller system prompts, fewer wasted calls
Parallel when independent β wall-clock savings
Rough cost per full feature (plan + design + build + review + 2 ingests): ~$0.18 Naive "opus everywhere" alternative: ~$0.90 β 5Γ cheaper with proper model routing.
FAQ
Common questions about INGEST, QUERY, LINT, PII, versioning, RAG comparison, and more β see FAQ.md.
Vault layout
vault/
βββ index.md β entry point, read first
βββ _schema/
β βββ template.md β required frontmatter for all notes
β βββ conventions.md β naming, linking, tagging rules
β
βββ architecture/ β how the system is built
βββ decisions/ β why we chose X over Y (dated)
βββ design/ β UI/UX decisions
βββ entities/ β tools, services (stripe, supabase)
βββ concepts/ β cross-cutting ideas
βββ sessions/ β dated session summariesEvery note has frontmatter. Every note links to others. Orphans are tech debt.
File structure
claude-obsidian-kit/
βββ README.md β you are here
βββ RETROFIT.md β pointing the kit at a folder you already have
βββ EFFICIENCY.md β token patterns (10 of them)
βββ FAQ.md β common questions
βββ LICENSE β MIT
βββ agents/
β βββ archivist.md β reader/guardian (QUERY + LINT)
β βββ scribe.md β dedicated writer (INGEST)
βββ skills/
β βββ llm-wiki/
β βββ SKILL.md β main skill manifest
β βββ ingest.md β INGEST protocol detail
β βββ query.md β QUERY protocol detail
β βββ lint.md β LINT protocol detail
βββ vault-template/
β βββ index.md β vault entry point
β βββ _schema/
β β βββ template.md β note template
β β βββ conventions.md β full rulebook
β βββ architecture/
β βββ decisions/
β βββ design/
β βββ entities/example-service.md
β βββ concepts/example-concept.md
β βββ sessions/
βββ obsidian/
β βββ graph.json β graph coloured by vault layer
β βββ snippets/
β β βββ vault-layers.css β [!conflict] callout + folder colours
β βββ README.md
βββ scripts/
βββ install.sh β one-command setup
βββ vault-lint.py β runs the LINT protocol for real (no deps)Works well with
Claude Code β the primary target
Obsidian β the vault viewer/editor (optional β vault is plain markdown)
VS Code, Typora, Bear β any markdown editor works
Previous kit: knowledge-pipeline β this extends it with the multi-agent efficiency layer
License
MIT
Credits
Built on Claude Code's agent + skill system.
Inspired by:
obra/superpowers β skills as methodology
anthropics/skills β skill file format
wshobson/agents β multi-agent orchestration patterns
Zettelkasten β atomic notes + aggressive linking
scripts/vault-lint.py
Shown as plain text.
#!/usr/bin/env python3
"""vault-lint β run the kit's LINT protocol for real.
`skills/llm-wiki/lint.md` specifies the checks and the health-score formula.
This script implements that spec: no dependencies, no network, read-only by
default. Point it at a vault and it prints the report format the skill asks for.
python3 vault-lint.py path/to/vault
python3 vault-lint.py . --json
python3 vault-lint.py . --fields tur,durum,guncelleme --conflict-callout conflict
Read-only unless you pass --write-report.
"""
from __future__ import annotations
import argparse
import fnmatch
import json
import os
import re
import sys
from collections import Counter, defaultdict
from datetime import date, datetime, timedelta
# --- link + frontmatter parsing -------------------------------------------------
FENCE = re.compile(r"^(\s*)(```|~~~)")
INLINE_CODE = re.compile(r"`[^`\n]*`")
WIKILINK = re.compile(r"\[\[([^\[\]]+?)\]\]")
FM_DELIM = "---"
# Obsidian ignores links inside code. A linter that does not is a linter that
# cries wolf: `[[link]]` written as an example is not a broken link.
def strip_code(text: str) -> str:
out, in_fence, fence_tok = [], False, ""
for line in text.splitlines():
m = FENCE.match(line)
if m:
tok = m.group(2)
if not in_fence:
in_fence, fence_tok = True, tok
elif tok == fence_tok:
in_fence = False
out.append("")
continue
out.append("" if in_fence else INLINE_CODE.sub("", line))
return "\n".join(out)
def parse_frontmatter(text: str) -> dict:
lines = text.splitlines()
if not lines or lines[0].strip() != FM_DELIM:
return {}
body: list[str] = []
for line in lines[1:]:
if line.strip() == FM_DELIM:
break
body.append(line)
fm: dict[str, object] = {}
key = None
for line in body:
if re.match(r"^\s*-\s+", line) and key:
fm.setdefault(key, [])
if isinstance(fm[key], list):
fm[key].append(re.sub(r"^\s*-\s+", "", line).strip())
continue
m = re.match(r"^([A-Za-z_][\w-]*)\s*:\s*(.*)$", line)
if m:
key, val = m.group(1), m.group(2).strip()
fm[key] = val if val else []
return fm
def link_targets(text: str):
"""Yield (target, raw) for each wikilink, normalised the way Obsidian resolves."""
for m in WIKILINK.finditer(strip_code(text)):
raw = m.group(1)
# alias: [[target|Alias]]. Inside a markdown table the pipe is written
# \| to escape the cell separator, but it is still the alias pipe.
target = raw.replace("\\|", "|").split("|")[0].strip()
target = target.split("#")[0].split("^")[0].strip()
if target:
yield target, raw
# --- vault model ----------------------------------------------------------------
class Vault:
"""Two distinct sets, and the difference is the whole point when you retrofit:
- `notes` β what gets audited (default: every .md; narrow it with --notes)
- `all_files` β what links resolve against (always the whole vault)
Point a linter at a folder holding six months of raw material and it will
scold you for every `[[example]]` in a tutorial you saved. Audit the wiki
layer, resolve against everything.
"""
def __init__(self, root: str, excludes: list[str], note_globs: list[str] | None = None):
self.root = os.path.abspath(root)
self.excludes = excludes + [".obsidian/*", ".git/*", "node_modules/*"]
self.note_globs = note_globs or []
self.notes: dict[str, str] = {} # audited
self.all_files: set[str] = set() # resolvable
self._load()
def _audited(self, rel: str) -> bool:
if not self.note_globs:
return True
r = rel.replace(os.sep, "/")
return any(fnmatch.fnmatch(r, g) or r.startswith(g.rstrip("*").rstrip("/") + "/")
for g in self.note_globs)
def _excluded(self, rel: str) -> bool:
return any(fnmatch.fnmatch(rel, p) or rel.startswith(p.rstrip("*"))
for p in self.excludes)
def _load(self):
for dirpath, dirnames, filenames in os.walk(self.root):
dirnames[:] = [d for d in dirnames if d not in (".git", ".obsidian", "node_modules")]
for fn in filenames:
full = os.path.join(dirpath, fn)
rel = os.path.relpath(full, self.root)
if self._excluded(rel):
continue
self.all_files.add(rel)
if fn.endswith(".md") and self._audited(rel):
try:
self.notes[rel] = open(full, encoding="utf-8", errors="replace").read()
except OSError:
pass
def resolve(self, target: str, from_rel: str = "") -> tuple:
"""Mimic Obsidian's resolution. Returns (path_or_None, ambiguous).
Obsidian does not pick at random when several files share a basename --
it prefers the closest one. Neither should a linter: choosing arbitrarily
reports phantom orphans for the page it did not choose.
Order: exact relative path -> same folder as the linking note -> shallowest
path -> lexicographic. `ambiguous` is True when the basename was not unique.
"""
norm = target.replace("\\", "/").lower()
for cand in (norm, norm + ".md"):
for rel in sorted(self.all_files):
if rel.replace(os.sep, "/").lower() == cand:
return rel, False
base = os.path.basename(norm)
matches = []
for rel in self.all_files:
name = os.path.basename(rel).lower()
stem = name[:-3] if name.endswith(".md") else name
if stem == base or name == base:
matches.append(rel)
if not matches:
return None, False
if len(matches) == 1:
return matches[0], False
here = os.path.dirname(from_rel)
matches.sort(key=lambda r: (os.path.dirname(r) != here, r.count(os.sep), r.lower()))
return matches[0], True
# --- checks ---------------------------------------------------------------------
def run(v: Vault, args) -> dict:
required = [f.strip() for f in args.fields.split(",") if f.strip()]
src_fields = [f.strip() for f in args.source_fields.split(",") if f.strip()]
inbound: dict[str, set[str]] = defaultdict(set)
broken: list[tuple[str, str]] = []
ambiguous: Counter = Counter()
unresolved_counts: Counter = Counter()
total_links = 0
for rel, text in v.notes.items():
for target, _raw in link_targets(text):
total_links += 1
hit, amb = v.resolve(target, rel)
if amb:
ambiguous[target] += 1
if hit:
if hit != rel:
inbound[hit].add(rel)
else:
broken.append((rel, target))
unresolved_counts[target] += 1
orphans = sorted(r for r in v.notes if not inbound.get(r) and not _is_entry(r))
fm_violations = []
stale = []
bad_sources = []
cutoff = date.today() - timedelta(days=args.stale_days)
for rel, text in v.notes.items():
fm = parse_frontmatter(text)
missing = [] if _is_entry(rel) else [f for f in required if f not in fm]
if missing:
fm_violations.append((rel, missing))
d = _as_date(fm.get(args.date_field) if args.date_field in fm else fm.get("date"))
status = str(fm.get("status") or fm.get("durum") or "").strip().lower()
if d and d < cutoff and status in args.stale_status.split(","):
stale.append((rel, d.isoformat(), status))
for sf in src_fields:
val = fm.get(sf)
for p in (val if isinstance(val, list) else [val] if val else []):
p = str(p).strip().strip("'\"")
if not p or p.startswith(("http://", "https://")):
continue
# "user | agent | doc" or "<where this came from>" are placeholders,
# not paths. Do not report a template as a fabricated citation.
if "|" in p or p.startswith("<") or " or " in p:
continue
# External references (absolute/home paths, "Label: /some/where")
# are outside the vault by definition. Only vault-relative paths
# are claims this tool can check.
if p.startswith(("~", "/")) or ": " in p:
continue
# `source:` may hold provenance ("user", "planner") rather than a
# location. Only verify values that look like a path: a separator,
# or a file extension.
if "/" not in p and not re.search(r"\.[A-Za-z0-9]{1,5}$", p):
continue
if not os.path.exists(os.path.join(v.root, p)):
bad_sources.append((rel, p))
missing_concepts = [(t, c) for t, c in unresolved_counts.most_common()
if c >= args.concept_threshold]
conflicts = []
pat = re.compile(r">\s*\[!" + re.escape(args.conflict_callout) + r"\]", re.I)
for rel, text in v.notes.items():
n = len(pat.findall(text))
if n:
conflicts.append((rel, n))
hubs = sorted(((len(s), r) for r, s in inbound.items()), reverse=True)[:10]
score = max(0, min(100, 100
- len(broken) * 10
- len(orphans) * 3
- len(stale) * 1
- len(missing_concepts) * 5
- (sum(n for _, n in conflicts) * 20 if args.score_conflicts else 0)
- len(fm_violations) * 5))
return {
"vault": v.root,
"generated": date.today().isoformat(),
"notes": len(v.notes),
"files": len(v.all_files),
"links": total_links,
"links_per_note": round(total_links / len(v.notes), 1) if v.notes else 0,
"score": score,
"broken": broken,
"orphans": orphans,
"stale": stale,
"missing_concepts": missing_concepts,
"frontmatter_violations": fm_violations,
"conflicts": conflicts,
"bad_sources": bad_sources,
"hubs": hubs,
"ambiguous": ambiguous.most_common(),
}
def _is_entry(rel: str) -> bool:
return os.path.basename(rel).lower() in ("index.md", "readme.md", "log.md", "moc.md")
def _as_date(val):
if not val or isinstance(val, list):
return None
m = re.search(r"(\d{4})-(\d{2})-(\d{2})", str(val))
if not m:
return None
try:
return datetime(int(m.group(1)), int(m.group(2)), int(m.group(3))).date()
except ValueError:
return None
# --- report ---------------------------------------------------------------------
BANDS = [(90, "β
Healthy"), (70, "β οΈ Minor debt"), (50, "β οΈ Needs attention"), (0, "π¨ Cleanup overdue")]
def band(score: int) -> str:
return next(label for threshold, label in BANDS if score >= threshold)
def render(r: dict) -> str:
o = []
a = o.append
a(f"# Vault Health Report β {r['generated']}\n")
a(f"**Vault:** `{r['vault']}` ")
a(f"**Total notes:** {r['notes']} ")
a(f"**Total links:** {r['links']} ({r['links_per_note']} per note) ")
a(f"**Health score:** {r['score']} / 100 β {band(r['score'])}\n")
if r["conflicts"]:
total = sum(n for _, n in r["conflicts"])
a(f"## CRITICAL\n\n### Flagged contradictions ({total} across {len(r['conflicts'])} notes)\n")
a("These are *marked*, not silently resolved β that is the protocol working. "
"They still need a human decision.\n")
for rel, n in sorted(r["conflicts"], key=lambda x: -x[1])[:15]:
a(f"- `{rel}` β {n}")
a("")
if r["broken"] or r["bad_sources"]:
a("## HIGH\n")
if r["broken"]:
a(f"### Broken links ({len(r['broken'])})\n")
for rel, t in r["broken"][:25]:
a(f"- `{rel}` β `[[{t}]]`\n - *Suggestion: create the stub, or fix the link text.*")
if len(r["broken"]) > 25:
a(f"- β¦and {len(r['broken']) - 25} more")
a("")
if r["bad_sources"]:
a(f"### Unverifiable sources ({len(r['bad_sources'])})\n")
a("A note cites a source path that does not exist on disk. This is how you catch "
"a citation the agent invented.\n")
for rel, p in r["bad_sources"][:25]:
a(f"- `{rel}` cites `{p}` β not found")
a("")
if r["orphans"] or r["missing_concepts"] or r["frontmatter_violations"]:
a("## MEDIUM\n")
if r["orphans"]:
a(f"### Orphan notes ({len(r['orphans'])})\n")
for rel in r["orphans"][:25]:
a(f"- `{rel}` β no incoming links\n - *Suggestion: link from a parent concept, or archive.*")
a("")
if r["missing_concepts"]:
a(f"### Missing concept pages ({len(r['missing_concepts'])})\n")
for t, c in r["missing_concepts"][:25]:
a(f"- `[[{t}]]` β referenced {c}Γ, no page\n - *Suggestion: INGEST a page synthesising existing references.*")
a("")
if r["frontmatter_violations"]:
a(f"### Frontmatter violations ({len(r['frontmatter_violations'])})\n")
for rel, missing in r["frontmatter_violations"][:25]:
a(f"- `{rel}` β missing: {', '.join(missing)}")
a("")
if r["ambiguous"]:
a("### Ambiguous link targets (%d)\n" % len(r["ambiguous"]))
a("Several files share this basename, so `[[name]]` resolves by proximity - "
"which may not be the file you meant. Disambiguate with a path link.\n")
for t, c in r["ambiguous"][:15]:
a("- `[[%s]]` - %d x ambiguous" % (t, c))
a("")
if r["stale"]:
a(f"## LOW\n\n### Stale notes ({len(r['stale'])})\n")
for rel, d, st in r["stale"][:25]:
a(f"- `{rel}` β {d}, status `{st}`\n - *Suggestion: accept, deprecate, or update.*")
a("")
if r["hubs"]:
a("## Hubs β most linked-to notes\n")
a("| Note | Inbound |")
a("|---|---|")
for n, rel in r["hubs"]:
a(f"| `{rel}` | {n} |")
a("")
a("---\n")
a("Generated by [`vault-lint.py`](scripts/vault-lint.py) β "
"[claude-obsidian-kit](https://github.com/selmakcby/claude-obsidian-kit)")
return "\n".join(o)
def main() -> int:
p = argparse.ArgumentParser(description="Run the llm-wiki LINT protocol against a vault.")
p.add_argument("vault", nargs="?", default=".", help="path to the vault root")
p.add_argument("--fields", default="type,date,status",
help="required frontmatter fields (default: type,date,status)")
p.add_argument("--date-field", default="date", help="frontmatter field holding the date")
p.add_argument("--source-fields", default="source,kaynak",
help="frontmatter fields holding source paths to verify on disk")
p.add_argument("--conflict-callout", default="conflict",
help="callout name used to flag contradictions")
p.add_argument("--score-conflicts", action="store_true",
help="penalise flagged contradictions (-20 each), per the strict lint.md "
"formula. Off by default: a flagged conflict is the protocol working, "
"not a defect. An *unflagged* one is the defect, and no linter sees those.")
p.add_argument("--concept-threshold", type=int, default=3,
help="unresolved link seen N+ times becomes a missing-concept finding")
p.add_argument("--stale-days", type=int, default=90)
p.add_argument("--stale-status", default="draft,wip,taslak")
p.add_argument("--notes", action="append", default=[], metavar="GLOB",
help="restrict the audit to these paths (repeatable, e.g. --notes 'wiki/*'). "
"Links still resolve against the whole vault.")
p.add_argument("--exclude", action="append", default=[], metavar="GLOB")
p.add_argument("--json", action="store_true", help="emit raw findings as JSON")
p.add_argument("--write-report", metavar="FILE",
help="also write the markdown report to FILE (the only write this tool makes)")
p.add_argument("--fail-under", type=int, default=None,
help="exit 1 if the health score is below this (for CI)")
args = p.parse_args()
if not os.path.isdir(args.vault):
print(f"vault-lint: not a directory: {args.vault}", file=sys.stderr)
return 2
v = Vault(args.vault, args.exclude, args.notes)
if not v.notes:
print(f"vault-lint: no markdown notes found under {v.root}", file=sys.stderr)
return 2
r = run(v, args)
print(json.dumps(r, indent=2, ensure_ascii=False, default=list) if args.json else render(r))
if args.write_report:
with open(args.write_report, "w", encoding="utf-8") as fh:
fh.write(render(r) + "\n")
print(f"\nreport written to {args.write_report}", file=sys.stderr)
if args.fail_under is not None and r["score"] < args.fail_under:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
31 files
- .gitignore
- ATTRIBUTION.md
- AUTOGINEER-RUNS.md
- CLAUDE.tr.md
- EFFICIENCY.md
- FAQ.md
- LICENSE
- PROMPTS.md
- README.md
- RETROFIT.md
- agents/archivist.md
- agents/scribe.md
- obsidian/README.md
- obsidian/graph.json
- obsidian/snippets/vault-layers.css
- scripts/install.sh
- scripts/vault-lint.py
- skills/llm-wiki/SKILL.md
- skills/llm-wiki/ingest.md
- skills/llm-wiki/lint.md
- skills/llm-wiki/query.md
- vault-template/CLAUDE.md
- vault-template/_schema/conventions.md
- vault-template/_schema/template.md
- vault-template/architecture/overview.md
- vault-template/concepts/example-concept.md
- vault-template/decisions/.gitkeep
- vault-template/design/.gitkeep
- vault-template/entities/example-service.md
- vault-template/index.md
- vault-template/sessions/.gitkeep
Advantages
- Reduces token costs by using specialized models for different tasks, such as using Haiku for writes and Sonnet for reads. evidence: README.md
- Ensures knowledge persists across sessions, preventing the loss of project reasoning. evidence: README.md
- Provides a robust linting protocol to maintain vault health and catch fabricated citations. evidence: README.md
Limitations
- Requires manual setup and integration into existing project structures. evidence: README.md
- Relies on specific agent roles which must be correctly configured within the Claude Code environment. evidence: README.md
Security
Risk level: Low risk
This brain runs
Install scripts (1)
scripts/install.sh:sh scripts/install.sh
- Low risk: scripts-present (scripts/install.sh)
Attribution and licence
- First committed by
selmakcby
- Publisher
- selma (Community)
- Source
- selmakcby/claude-obsidian-kit Β· commit 2826401
- Licence
- MIT License (files served here)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Contributors
- selmakcby
AutoGineer is not affiliated with or endorsed by selmakcby.
Download
The ZIP holds the files AutoGineer serves, with ATTRIBUTION.md and the licence. Download claude-obsidian-kit.zip