obsidian-mind
Vault template
First committed by breferrari
Published in breferrari/obsidian-mind. Publisher page
5,527 GitHub stars
What it is
A self-organizing Obsidian vault that gives AI coding agents persistent memory.
AutoGineer overview, from the brain’s own files
Obsidian Mind transforms an Obsidian vault into a persistent, structured brain for AI coding agents like Claude Code, Codex, and Gemini CLI. It solves the problem of agent amnesia by maintaining a hybrid memory model that combines file-based storage with semantic search capabilities. The system uses a set of lifecycle hooks to manage agent context, including project status, meeting notes, decisions, and performance tracking. By leveraging a structured folder hierarchy and automated indexing, it ensures that agents can recall past decisions, track project progress, and maintain context across sessions. The vault is designed to be self-maintaining, with automated hygiene checks and routing logic that keeps the knowledge base organized as it grows.
How it works
Memory model: Hybrid
AutoGineer overview, from the brain’s own files
The system uses a combination of Obsidian markdown files for storage and procedural hooks for management. Lifecycle hooks handle tasks like session initialization, content classification, and vault hygiene. The vault is organized into specific folders for work, performance, organizational data, and agent-specific knowledge. Semantic search is provided via an optional QMD integration, which uses local models to index and retrieve information. Agents interact with the vault through commands and MCP tools, allowing them to read, write, and update notes while the underlying scripts ensure that the vault remains structured and linked.
| Folder | Role | Files |
|---|---|---|
| .claude | agent-config | 230 |
| .codex | agent-config | 1 |
| .gemini | agent-config | 21 |
| .github | agent-config | 11 |
| .obsidian | obsidian-config | 1 |
| brain | memory | 6 |
| templates | templates | 5 |
| .claude-plugin | folder | 2 |
| .scripts | folder | 3 |
| .shardmind | folder | 6 |
| bases | folder | 9 |
| org | folder | 3 |
| perf | folder | 4 |
| reference | folder | 1 |
| thinking | folder | 1 |
| work | folder | 6 |
The agent reads
- .claude/rules/thinking.md evidence: AGENTS.md
- work/active/ evidence: AGENTS.md
- .claude/rules/brain-topics.md evidence: AGENTS.md
- .claude/rules/vault-from-another-repo.md evidence: AGENTS.md
- perf/evidence/ evidence: AGENTS.md
- perf/ evidence: AGENTS.md
- work/ evidence: AGENTS.md
- brain/ evidence: README.md
- AGENTS.md evidence: README.md
- .gemini/commands/ evidence: README.md
- .gemini/settings.json evidence: README.md
The agent writes
- work/ evidence: AGENTS.md
- work/Index.md evidence: AGENTS.md
- perf/Brag Doc.md evidence: AGENTS.md
- brain/ evidence: AGENTS.md
- brain/Memories.md evidence: AGENTS.md
- .shardmind/ evidence: README.md
- vault-manifest.json evidence: README.md
- AGENTS.md evidence: README.md
- record_work evidence: README.md
- perf/evidence/ evidence: README.md
- .claude/agents/ evidence: README.md
- .claude/scripts/ evidence: README.md
Requirements
Apps: Obsidian
Databases: Redis, SQLite
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Google Antigravity, Kiro, OpenCode, Amp, Roo Code, Cline, JetBrains Junie, Devin (ex-Windsurf)
Set it up
This brain runs code automatically: 14 hooks (claude-code, codex, gemini-cli). Review .claude/settings.json, .codex/hooks.json, .gemini/settings.json before opening the folder in Claude Code. No step below turns a hook on for you.
.claude/settings.json:node --disable-warning=ExperimentalWarning --experimental-strip-types "$(r="${CLAUDE_PROJECT_DIR:-.}"; d="$r"; while [ ! -f "$d/vault-manifest.json" ] && p="$(dirname "$d")" && [ "$d" != "$p" ]; do d="$p"; done; [ -f "$d/vault-manifest.json" ] || d="$r"; printf %s "$d")/.claude/scripts/ …[+17 chars].claude/settings.json:node --disable-warning=ExperimentalWarning --experimental-strip-types "$(r="${CLAUDE_PROJECT_DIR:-.}"; d="$r"; while [ ! -f "$d/vault-manifest.json" ] && p="$(dirname "$d")" && [ "$d" != "$p" ]; do d="$p"; done; [ -f "$d/vault-manifest.json" ] || d="$r"; printf %s "$d")/.claude/scripts/ …[+20 chars].claude/settings.json:node --disable-warning=ExperimentalWarning --experimental-strip-types "$(r="${CLAUDE_PROJECT_DIR:-.}"; d="$r"; while [ ! -f "$d/vault-manifest.json" ] && p="$(dirname "$d")" && [ "$d" != "$p" ]; do d="$p"; done; [ -f "$d/vault-manifest.json" ] || d="$r"; printf %s "$d")/.claude/scripts/ …[+18 chars].claude/settings.json:node --disable-warning=ExperimentalWarning --experimental-strip-types "$(r="${CLAUDE_PROJECT_DIR:-.}"; d="$r"; while [ ! -f "$d/vault-manifest.json" ] && p="$(dirname "$d")" && [ "$d" != "$p" ]; do d="$p"; done; [ -f "$d/vault-manifest.json" ] || d="$r"; printf %s "$d")/.claude/scripts/ …[+15 chars].claude/settings.json:node --disable-warning=ExperimentalWarning --experimental-strip-types "$(r="${CLAUDE_PROJECT_DIR:-.}"; d="$r"; while [ ! -f "$d/vault-manifest.json" ] && p="$(dirname "$d")" && [ "$d" != "$p" ]; do d="$p"; done; [ -f "$d/vault-manifest.json" ] || d="$r"; printf %s "$d")/.claude/scripts/ …[+18 chars].codex/hooks.json:node --disable-warning=ExperimentalWarning --experimental-strip-types "${CODEX_PROJECT_DIR:-.}/.claude/scripts/session-start.ts".codex/hooks.json:node --disable-warning=ExperimentalWarning --experimental-strip-types "${CODEX_PROJECT_DIR:-.}/.claude/scripts/classify-message.ts".codex/hooks.json:node --disable-warning=ExperimentalWarning --experimental-strip-types "${CODEX_PROJECT_DIR:-.}/.claude/scripts/validate-write.ts".codex/hooks.json:node --disable-warning=ExperimentalWarning --experimental-strip-types "${CODEX_PROJECT_DIR:-.}/.claude/scripts/stop-checklist.ts".gemini/settings.json:node --disable-warning=ExperimentalWarning --experimental-strip-types .claude/scripts/session-start.ts.gemini/settings.json:node --disable-warning=ExperimentalWarning --experimental-strip-types .claude/scripts/classify-message.ts.gemini/settings.json:node --disable-warning=ExperimentalWarning --experimental-strip-types .claude/scripts/validate-write.ts.gemini/settings.json:node --disable-warning=ExperimentalWarning --experimental-strip-types .claude/scripts/pre-compact.ts.gemini/settings.json:node --disable-warning=ExperimentalWarning --experimental-strip-types .claude/scripts/stop-checklist.ts
Clone the repository at the commit AutoGineer scanned:
git clone https://github.com/breferrari/obsidian-mind.git && cd obsidian-mind && git checkout fae7be467fc5e23bf9d51df7207d41f0c07e2754In Obsidian, choose “Open folder as vault” and pick the folder.
Point Claude Code at .claude-plugin/marketplace.json: it does not read that file on its own.
cd obsidian-mind && claude
AutoGineer scanned these files at commit fae7be4.
- Not scanned by AutoGineerThe author’s MCP configuration (README.md) also starts it in a way AutoGineer does not offer. Read that file: AutoGineer did not scan that path; what it runs is listed under “This brain runs”. https://github.com/breferrari/obsidian-mind/blob/fae7be467fc5e23bf9d51df7207d41f0c07e2754/README.md
- 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/breferrari/obsidian-mind/blob/fae7be467fc5e23bf9d51df7207d41f0c07e2754/README.md#-install-via-shardmind-recommended
- Not scanned by AutoGineerThe author also offers https://github.com/breferrari/obsidian-mind/tree/fae7be467fc5e23bf9d51df7207d41f0c07e2754. https://github.com/breferrari/obsidian-mind/tree/fae7be467fc5e23bf9d51df7207d41f0c07e2754
First committed by breferrari. Published in breferrari/obsidian-mind. Licence: MIT License.
Usage
AutoGineer overview, from the brain’s own files
- Start a new session to load project context and North Star goals.
- Capture meeting takeaways and decisions using automated commands.
- Query the vault for past decisions or project history using semantic search.
- Perform end-of-day wrap-ups to index new notes and identify uncaptured wins.
- Manage incident response and documentation within the vault structure.
- Maintain performance records and brag documents for review cycles.
- “/om-standup”
- The agent loads the North Star goals, active projects, and recent tasks to provide a summary of current work status.
- “/om-dump Just had a 1:1 with Sarah. She wants error monitoring added before release.”
- The agent updates the person note for Sarah, creates a meeting record, and adds an error monitoring task to the active project note.
- “What did we decide about caching?”
- The agent uses semantic search to retrieve the specific decision record regarding the Redis migration.
- “wrap up”
- The agent verifies note links, updates indexes, and identifies uncaptured wins for the brag document.
Documentation by breferrari
<p align="center"> <img src="obsidian-mind-logo.png" alt="Obsidian Mind" width="120"> </p>
<h1 align="center">Obsidian Mind</h1>
Image: Claude Code Image: Codex CLI Image: Gemini CLI Image: Obsidian Image: Obsidian CLI Image: Obsidian Skills Image: QMD Image: Node Image: License
An Obsidian vault that gives AI coding agents persistent memory. Built for Claude Code, with working hooks for Codex CLI and Gemini CLI. Start a session, talk about your day, and the agent handles the rest — notes, links, indexes, performance tracking. Every conversation builds on the last.
🔴 The Problem
AI coding agents are powerful, but they forget. Every session starts from zero — no context on your goals, your team, your patterns, your wins. You re-explain the same things. You lose decisions made three conversations ago. The knowledge never compounds.
🟢 The Solution
Give your agent a brain.
You: "start session"
Agent: *reads North Star, checks active projects, scans recent memories*
Agent: "You're working on Project Alpha, blocked on the BE contract.
Last session you decided to split the coordinator. Your 1:1
with your manager is tomorrow — review brief is ready."Works with Claude Code (full support), Codex CLI, and Gemini CLI — same hooks, same commands, same vault.
Install via shardmind install or git clone — same vault either way.
⚡ See It In Action
<p align="center"> <img src="obsidian-mind-demo.gif" alt="Obsidian Mind demo — standup and dump commands" width="800"> </p>
Morning kickoff:
/om-standup
# → loads North Star, active projects, open tasks, recent git changes
# → "You have 2 active projects. The auth refactor is blocked on API contract.
# Your 1:1 with Sarah is at 2pm — last time she flagged observability."Brain dump after a meeting:
/om-dump Just had a 1:1 with Sarah. She's happy with the auth work but wants
us to add error monitoring before release. Also, Tom mentioned the cache
migration is deferred to Q2 — we decided to focus on the API contract first.
Decision: defer Redis migration. Win: Sarah praised the auth architecture.→ Updated org/people/Sarah Chen.md with meeting context
→ Created work/1-1/Sarah 2026-03-26.md with key takeaways
→ Created Decision Record: "Defer Redis migration to Q2"
→ Added to perf/Brag Doc.md: "Auth architecture praised by manager"
→ Updated work/active/Auth Refactor.md with error monitoring taskIncident response:
/om-incident-capture https://slack.com/archives/C0INCIDENT/p123456
# → slack-archaeologist reads every message, thread, and profile
# → people-profiler creates notes for new people involved
# → Full timeline, root cause analysis, brag doc entryEnd of day:
You: "wrap up"
# → verifies all notes have links
# → updates indexes
# → brag-spotter finds uncaptured wins
# → suggests improvements🚀 Quick Start
📦 Install via ShardMind (recommended)
npm install -g shardmind
mkdir my-vault && cd my-vault
shardmind install github:breferrari/obsidian-mindshardmind install writes into the current directory, so create and enter a fresh folder first. The wizard collects your name, organization, vault purpose, agents to include, and whether to enable QMD; ShardMind then initializes git, optionally bootstraps QMD, and personalizes brain/North Star.md with your answers. Then:
Open the installed folder as an Obsidian vault
Enable the Obsidian CLI in Settings → General (requires Obsidian 1.12+)
Run your agent in the vault directory:
claude,codex, orgemini. Codex runs the vault's hooks only once the folder is trusted and each hook approved: see Using with Other Agents.Start talking about work
ShardMind is the package manager for Obsidian vault templates. The install adds a .shardmind/ sidecar that powers the wizard, optional modules (skip what you don't use), and three-way-merge upgrades. With every value at its default the install is byte-equivalent to git clone — clone-UX is preserved exactly. Delete .shardmind/ and shard-values.yaml from the installed vault and it keeps working: ShardMind is additive, not load-bearing.
Or clone directly
git clone https://github.com/breferrari/obsidian-mind.gitOr use it as a GitHub template. Skip the wizard, get the bare template. Then run through the same 4 steps above, plus fill in brain/North Star.md with your goals (the ShardMind wizard does this for you).
🔍 Recommended: QMD Semantic Search
QMD is where most of the agent's retrieval intelligence comes from. Optional in the strict sense — the vault falls back to grep + the Obsidian CLI — but the experience is meaningfully better with it:
Semantic recall. Find "what did we decide about caching" even when the note is titled "Redis Migration ADR."
Brain topics available on demand. Your agent is instructed (via
AGENTS.md, which points to.claude/rules/brain-topics.md) to consultbrain/guidance through QMD when the conversation touches a listed topic.Subagents get sharper context.
context-loader,review-prep,brag-spotter, and friends consult QMD first, then fall back to grep.Native agent tools via MCP. Registered as a Model Context Protocol server in
.mcp.json— when QMD is installed,mcp__qmd__query,mcp__qmd__get, andmcp__qmd__multi_getappear in the agent's tool menu alongside Read and Edit. Subagents, slash commands, and the main conversation all call the same typed contract. Add another MCP-aware tool later (a database, a ticketing system, a calendar) and it plugs in the same way.
npm install -g @tobilu/qmd
node --experimental-strip-types .scripts/qmd-bootstrap.tsThe bootstrap is idempotent — safe to re-run. It resolves this vault's index name — the qmd_index field from vault-manifest.json when set, otherwise the vault folder name slugified — reads qmd_context, registers the collection, attaches the context, and builds the index + embeddings. The SessionStart hook and .mcp.json wrapper both read the same manifest field, so CLI queries, the MCP server, and the re-index all scope to the same named SQLite store. This isolates the vault from any other QMD-using vault on the same machine.
If you want to use a different index name (for example, one vault per engineer on a shared workstation), edit qmd_index in vault-manifest.json before running the bootstrap. Once the store is populated, always pass --index <name> to the CLI:
qmd --index obsidian-mind query "what did we decide about caching"
qmd --index obsidian-mind update # after bulk edits
qmd --index obsidian-mind embed # after many new notesHow it works under the hood
QMD runs three small models locally, so there is no API key to set up, no per-query cost, and it works offline:
| model | size | job |
|---|---|---|
embeddinggemma-300M | ~328MB | turns notes and queries into vectors |
qmd-query-expansion-1.7B | ~1.28GB | rewrites your query into better search terms |
Qwen3-Reranker-0.6B | ~640MB | reorders the shortlist by actual relevance |
They download on first use and are cached. QMD offloads to the GPU when it finds one — CUDA on a discrete card, Metal on Apple Silicon — and falls back to CPU otherwise. Check what yours is doing with qmd doctor.
Use qmd 2.5.1 or later. In 2.1, qmd status probes the GPU, and on a machine without one that can take several seconds (4 to 37 s on CI runners, measured 2026-10-13, #349). SessionStart's self-heal reads qmd status under a 5-second limit, so a status that times out leaves it unable to see a broken install.
The three CLI verbs map onto that stack, cheapest first: qmd search is BM25 keywords with no model at all, qmd vsearch is vector-only, and qmd query is the full hybrid. If you want to avoid the larger downloads, search alone is genuinely useful.
What that means for the MCP server
The om server sends a lexical and a vector sub-query, so retrieval finds the note that answers your question even when it shares no keywords with it. Practical consequences worth knowing:
Reads are the expensive side, not writes. A query embeds locally before it can search, so
recallwith a query takes a couple of seconds whilerecallwithout one is near-instant.searchover notes is fast — it is the vector step that costs.Writing a memory does not wait for the model. The index update is synchronous, so a new memory is immediately retrievable; generating its vector happens in the background, because that only affects where it ranks, not whether it is found.
No index, no problem. Without QMD the server falls back to lexical matching. Ordering gets worse; nothing disappears.
NOTE
If QMD isn't installed, everything still works — the agent falls back to grep and the Obsidian CLI, and the MCP server entry is skipped with a harmless warning.
📋 Requirements
Obsidian 1.12+ (for CLI support)
An AI coding agent: Claude Code (full support), Codex CLI, or Gemini CLI
Node 22+ LTS (for hook scripts — typically already installed alongside Claude Code / Codex / Gemini CLI)
Git (for version history)
QMD (optional, for semantic search)
Note on the Node flag. Hook scripts execute TypeScript directly via Node's
--experimental-strip-typesflag, stable in Node 22.6+ (Aug 2024) and the default behaviour in Node 23.6+. The flag is marked experimental but has been unchanged across 22 LTS and 24 LTS; if a future Node release retires or renames it, hook commands in.claude/settings.json,.codex/hooks.json, and.gemini/settings.jsonneed a one-line update.
⚙️ How It Works
Procedural code owns the environment. The agent owns content. The hooks in .claude/scripts/ handle classification, validation, indexing, and lifecycle injection — deterministic, testable, runs the same for every agent. Writing notes, filing them, linking them, drafting briefs — those are judgments, and they stay with the agent. The two halves meet at small handoffs (hooks inject context, agent reads the vault) so neither has to do the other's job.
Folders group by purpose. Links group by meaning. A note lives in one folder (its home) but links to many notes (its context). Your agent maintains this graph — linking work notes to people, decisions, and competencies automatically. When review season arrives, the backlinks on each competency note are already the evidence trail. A note without links is a bug.
Vault-first memory keeps context across sessions and machines. All durable knowledge lives in brain/ topic notes (git-tracked, Obsidian-browsable, linked). Claude Code's MEMORY.md (~/.claude/) is an auto-loaded index that points to vault locations — never the storage itself. This means memories survive machine changes and are part of the graph.
Sessions have a designed lifecycle. The SessionStart hook auto-injects your North Star goals, active projects, recent changes, open tasks, and the full vault file listing — your agent starts every session with context, not a blank slate. At the end, say "wrap up" and the agent runs /om-wrap-up — verifying notes, updating indexes, and spotting uncaptured wins. The AGENTS.md operating manual governs everything in between: where to file things, how to link, when to split a note, what to do with decisions and incidents.
🔗 Hooks
The lifecycle hooks handle routing automatically:
| Hook | When | What |
|---|---|---|
| 🚀 SessionStart | On startup/resume | QMD re-index + self-heal, inject North Star focus, active work, recent changes, tasks, file listing, vault-hygiene drift flags — held under a byte budget that fits Claude Code's hook output cap, ending with an injection-size meter |
| 💬 UserPromptSubmit | Every message | Classifies content (decision, incident, win, 1:1, architecture, person, project update) and injects routing hints; also hands the agent the Stop report from the previous turn |
| ✍️ PostToolUse | After writing .md | Validates frontmatter + wikilinks, blocks misplaced memory files, flags oversized notes (split, don't trim) and write-time topic clusters |
| 💾 PreCompact | Before context compaction | Backs up session transcript to thinking/session-logs/ |
| 🏁 Stop | After every response | Checklist + concrete drift findings (same hygiene scan as SessionStart), shown once per session and again only when they change: you see a short summary, one line per section, and the agent gets the full report with your next message and decides whether to act; hands drift to om-tidy |
TIP
You just talk. The hooks handle the routing.
🧩 The Claude Code mod
On Claude Code 2.1.287 or later, the vault also ships a mod: .claude/skills/obsidian-mind/, a plugin whose code runs inside Claude Code. It runs the vault's own hook scripts and changes only how their output reaches the session:
Session context arrives as an instruction file, the way
AGENTS.mddoes. It is re-read whole after compaction and/clear(as hook output it shrinks to a pointer), it reaches general-purpose subagents (hook output never does), and it is not cut at Claude Code's 10,000-character hook limit. Its budget iseager_layer_instruction_budget_bytesinvault-manifest.json; sections that never shrink, such as open tasks, can still take it past that./memorylists it as.claude/session-context.md.The Stop report becomes one line under the answer. When the findings change you see a line beneath Claude's reply, starting with the mod's name —
obsidian-mind: vault check: …. Claude Code joins the names of the plugins that answered on Stop with+, so when another plugin also answers, the label readsobsidian-mind+<plugin>: vault check: …. Claude gets the full report with your next message, unseen. A finding marked urgent gets Claude's attention at once instead, once per message you send (a second one waits inside the report); the template's own report has none.
For each event it handles, the mod tells the matching hook to stand down. Wherever the mod does not load, the hooks run exactly as before: Codex and Gemini, older Claude Code, a session started in a vault subfolder (launch from the vault root, or /cd there and /clear), or a folder you have not trusted. It loads only after you accept Claude Code's trust prompt for the vault.
A mod is unsandboxed code that runs with your permissions, so check what it does before trusting the folder: claude plugin validate .claude/skills/obsidian-mind lists every event it hooks and every call it makes (it runs the vault's own scripts, writes the context file, remembers in its own store which report each session was shown, and can submit a prompt for an urgent finding, nothing else). To turn it off, add "enabledPlugins": { "obsidian-mind@skills-dir": false } to .claude/settings.local.json.
<!-- mod-validate:start -->
The two lines that matter in its output, for this version of the mod:
❯ ./register.ts hooks: classic.SessionStart, prompt.context, classic.Stop, turn.complete, prompt.submit, turn.start
❯ ./register.ts calls: $.fs.write, $.process.run (via runScript), $.prompt.submit, $.session.root, $.state.get, $.state.set, $.store.get (via setShown, shownFor), $.store.set (via setShown), $.ui.invalidate<!-- mod-validate:end -->
⚡ Token Efficiency
obsidian-mind does not dump your entire vault into context. It uses tiered loading to keep token costs low:
| Tier | What | When | Cost |
|---|---|---|---|
| Always | AGENTS.md (plus .claude/rules/ on Claude Code) + SessionStart context (North Star excerpt, git summary, tasks, vault file listing) | Session start | capped by the manifest budget, itself held under Claude Code's 10,000-character hook output cap; the meter reports the real size every session |
| On-demand | QMD semantic search results | When the agent needs specific context | Targeted |
| Triggered | Classification routing hints | Every message | ~100 tokens |
| Triggered | PostToolUse validation | After .md writes | ~200 tokens |
| Rare | Full file reads | Only when explicitly needed | Variable |
SessionStart loads lightweight context — small excerpts from key files, filenames, and git summary — not full note contents. Five mechanisms keep the eager layer honest as the vault grows: source-aware injection (resume/compact re-inject only volatile sections — the static bulk is already in-conversation), an injection-size meter as the last line of every injection (you always see what context costs), an injection budget that enforces what the meter measures (over the ceiling, the cheapest-to-lose sections degrade to pointers — and the meter names every one it dropped, because a silent loss is worse than the bloat), a single hook spawn per write (the QMD refresh rides the validation hook), and listing collapse (any folder past a note-count threshold folds to one count line, so a vault can't outgrow the ceiling through whichever folder nobody thought to configure). Both the budget and the threshold are tunable in vault-manifest.json, and the budget is held under Claude Code's 10,000-character hook output cap whatever it is set to, because past the cap a session receives only a 2,000-character preview. The agent queries by meaning via QMD before reading files, so it pulls only what's relevant. The classification hook is one lightweight Node call per message. The validation hook only fires on markdown writes and skips excluded paths.
🌐 Using with Other Agents
obsidian-mind works with Claude Code, Codex CLI, Gemini CLI, Antigravity and Grok. Every agent reads one instruction file, AGENTS.md. The hook scripts in .claude/scripts/ and the commands in .claude/commands/ are agent-agnostic — pure Markdown, TypeScript, and shell with no SDK dependencies. .claude/rules/ holds guidance only Claude Code can use (its skills, subagents, auto-memory index, hooks and mod), and the sections AGENTS.md moved out to stay under its size cap. Claude Code and Grok load every file there at session start, so Claude keeps the whole manual; the other agents read a moved section when AGENTS.md points them to it.
| Agent | Reads AGENTS.md | From a vault subfolder | Also loads | Known gaps |
|---|---|---|---|---|
| Claude Code | Natively, when no CLAUDE.md is present | Yes, but @ imports are dropped (upstream) | .claude/rules/*.md | A CLAUDE.md beside or above AGENTS.md is read instead, silently: see Troubleshooting |
| Codex CLI | Natively | Yes (walks up to the git root) | — | Stops reading past ~32 KiB, silently |
| Gemini CLI | Through .gemini/settings.json (context.fileName) | No: see Troubleshooting | — | What a live session does with it is not yet observed (TBC) |
| Antigravity | Natively | Yes | — (not .claude/rules/, not CLAUDE.md) | Stops reading past ~24,000 bytes, silently. Has a workspace-trust dialog; whether it gates AGENTS.md is TBC (a headless -p run loaded it in a never-opened folder). Its .agents/ manifests are not exercised here (TBC) |
| Grok | Once the folder is trusted | Yes, once trusted | .claude/rules/*.md and CLAUDE.md, plus project .claude/settings.json hooks and user-level ~/.claude skills, permissions and hooks | An untrusted folder gets no project instructions at all: see Troubleshooting |
Observed with scratch vaults as of 2026-10-06 (Claude Code 2.1.291, Codex CLI 0.160.0, Gemini CLI 0.37.1 discovery log, Antigravity 1.3.0, Grok 1.0.46); the measurements are in #321. A test keeps AGENTS.md under 22,000 bytes, so no agent cuts it off.
Claude Code — full support. Hooks, commands, subagents, and the memory system all work out of the box.
Codex CLI — hook config at .codex/hooks.json wires the same hook scripts Claude Code uses — session context, message classification, and write validation work automatically. Commands work as regular prompts (e.g. type om-standup without the / prefix).
Codex runs a project's hooks only once two kinds of trust are in place (observed with Codex CLI 0.160.0 as of 2026-10-13, #345):
The vault folder is trusted. Codex records this in
~/.codex/config.tomlunder[projects.'<path>']. On Windows the key is the\\?\-prefixed path, e.g.[projects.'\\?\C:\path\to\vault'].Each hook is approved. Codex stores each approval as a
trusted_hashunder[hooks.state]. Approving them in an interactivecodexsession is expected to write these (inferred: the #345 check stood in for them with--dangerously-bypass-hook-trust, a flag for one non-interactive run).
Until both are in place, Codex runs no hook, and the session gets no vault context.
Gemini CLI — hook config at .gemini/settings.json maps Gemini's event names to the shared hook scripts, and its context.fileName setting points Gemini at AGENTS.md. Gemini reads custom commands only as TOML, so .gemini/commands/ holds a generated wrapper per command that injects the command's .md (node --experimental-strip-types .scripts/gemini-commands.ts regenerates them).
Antigravity and Grok — read AGENTS.md for vault conventions. Their hook, skill and command layers are tracked in #322 and #323.
Other agents (Cursor, Windsurf, GitHub Copilot, JetBrains AI) — read AGENTS.md for vault conventions. Hook support varies by agent.
NOTE
Hooks, commands, subagent prompts, and vault memory (brain/) are all agent-agnostic. Only the~/.claude/auto-memory loader and theobsidian-mindmod are Claude Code-specific..claude/rules/hooks-per-agent.mdmaps each hook to each agent's event.
🧠 Reach Your Vault From Any Repo
Your vault normally only helps while you are sitting in it. The om MCP server changes that: a coding session in any other repository can search your notes, read them, follow the graph, and record what it learned back into the vault.
Step 1 — register the server once, for every repo
claude mcp add --scope user om node "/absolute/path/to/your-vault/.claude/scripts/om-mcp.mjs"User scope is the right default here: it registers in your own config, so the server is available in every directory on the machine with nothing added to any repository. No env var is needed either, because the launcher resolves the vault from its own location.
<details> <summary>Per-repo alternative, and one trap to avoid</summary>
If you would rather a specific repo carry the wiring (so a teammate gets it on clone), put it in that project's .mcp.json:
{
"mcpServers": {
"om": {
"command": "node",
"args": ["/absolute/path/to/your-vault/.claude/scripts/om-mcp.mjs"]
}
}
}Note the cost: that path is absolute and machine-specific, so committing it breaks every collaborator and every other machine of yours.
Use an absolute path, and do not copy the relative one. This vault's own .mcp.json registers qmd with a relative path (.claude/scripts/qmd-mcp.mjs). That is correct there, because a relative path resolves against the current working directory and a session in the vault is already in it. Reused for om in a consuming project, the same shape silently resolves against that project instead, and the server never starts.
</details>
Step 2 — point the consuming project at the vault
Add this to that project's own instruction file (CLAUDE.md or AGENTS.md), filling in the triggers:
## Where design decisions live
Design rationale for this project is recorded outside this repo, reachable
through the `om` MCP server.
- **`search`** reaches the written record: why a choice was made, what was
rejected, what a constraint was set against. **Start here.**
- **`expand`** shows a known note's links and backlinks, which is cheaper than
searching again for its neighbourhood.
- **`recall`** returns short durable lessons scoped to this project. It is empty
until sessions put things in it, so early on it returns nothing, and that is
*not* evidence the record is missing.
- **`health`** when something that should be there cannot be found. Every failure
in this layer looks identical from outside (no results), and this tells them
apart.
Consult the record before changing:
- <the storage format or schema>
- <the ID or key semantics>
- <the public surface: CLI flags, API shape, exported names>
If that record and this repo disagree, the record holds the *why*. Reconcile
before changing behaviour.
**Say what came back, in whatever you write before implementing:** a plan, a
design note, an issue comment. Name the recorded decisions the work rests on,
anything you found that argues *against* the approach, and an explicit "nothing
recorded on this" when the record is empty, which is a finding rather than a
blank to skip. Consulting once at the start of a task is consulting at the
moment you know least about what you will need; writing the result down moves it
to the moment you commit, while a contradiction is still free to fix.
## Recording what you learn
Two tools, and picking the wrong one is the common mistake. The test is whether
it would help someone working on a **different** project.
- **`remember`** stores a durable lesson: a constraint you discovered, a gotcha
that cost time, a rule that generalises. Set `confidence`
(`verified` / `inferred` / `unverified`) honestly and supply `verification`
when you claim `verified`. For something specific to this project use
`scope: "project"` with `projects: ["<this-repo>"]`. Reach for
`scope: "platform"` before `"general"`: a dependency's quirk or a language's
rule is platform-level however hard-won, and `general` claims it would help
someone whose stack shares nothing with yours. When you do claim `general`,
supply `generality` saying why, the same way `verification` backs `verified`.
- **`record_work`** files what happened *here*: changes, decisions and the
alternatives rejected, what was learned, what is still open, how it was
verified.
A dependency limitation that would bite any project is a `remember`. "Landed the
watch engine and here is what it cost" is a `record_work`. Do both when both are
true.Three to five triggers, and name specific nouns. Consult it before changing the storage format, ID semantics, or the CLI surface works. Check the vault for context is exactly the advisory phrasing that gets skipped, because it gives the model nothing to match against the task in front of it.
Anchor the consultation to an artifact, not to a moment. "Consult before changing X" fires once, at the start of a task, which is when the session knows least about what it will need. Requiring the result to appear in whatever the project writes before implementing turns consultation from something a session intends to do into something an artifact is incomplete without. Measured across a day of real use: captures ran roughly four times heavier than consultations, because remember fires on an event (you just learned something) while consulting fires on intention. Event-triggered behaviour happens; intention-triggered behaviour decays.
TIP
Do not lead withrecall. It returns memories, and the store is empty on a project's first session by construction:rememberrefuses when called from inside the vault, since a memory written there would be scoped to the vault and reach nobody. So memories only ever arrive from outside. A snippet that promisesrecallwill return this project's decisions therefore returns nothing on the first run, and fails silently as "no results" rather than as an error.
IMPORTANT
Both steps are required, and the second is not paperwork. Measured: with the server wired and no repo-side instruction, a session made zero vault calls and implemented a design the vault had recorded as explicitly rejected. With the instruction present, it refused and cited the note.A prohibition in the MCP
instructionsfield propagates into the calling session reliably. A positive "go consult the vault" is advisory and gets skipped whenever a nearer source exists. The server can stop a session doing something; only the project's own law makes one go looking.That asymmetry decides which side a rule belongs on, and it matters most for anything you would rather not advertise. Writing "this repo deliberately excludes X" into a public repo announces the withholding and points at exactly what is being withheld. The same rule as a prohibition in
instructionsreaches every session invisibly, and the repo says nothing at all. Routing belongs repo-side because it has to; constraints belong server-side because they can.
What the session gets: search (semantic + keyword), expand (a note's links and backlinks), recall (durable lessons scoped to that repo), remember (record a lesson), record_work (file what happened), reason (judgement across several notes), and health (is the wiring intact?). Plus your notes as readable resources.
reason is the one that thinks. The others retrieve; this one reads the vault with a second Claude session and answers questions that need judgement across notes — is what I am about to do consistent with what these six notes decided? It seeds itself from search, so you never have to search first. It runs on your own CLI default model, so the vault answers at the level you are already working at, and nothing about it is capped: it is Claude, on your machine, under your auth. Every call is logged with its cost, turns, model and wall time. Its answers are marked confidence: inferred and are never recorded as memories on their own — your session decides whether any of it is worth keeping.
Repos are identified by folder name, which is right until it isn't — two repos both called api share one identity and therefore each other's memories. Drop a .om-project file with a distinct name at the repo root to separate them; health tells you which repo it thinks is calling and where that name came from.
Cross-repo memory with an epistemic contract. A lesson learned in one repo reaches the repos it was scoped to and no others. Reach is declared at write time, never guessed at read time, so a sibling project does not inherit another's constraints. Everything recorded carries confidence: verified | inferred | unverified, dated volatile facts, and provenance derived server-side — a session cannot claim to be a project it is not. Corrections supersede rather than overwrite, so the store gets more trustworthy as it grows instead of accumulating contradictions.
Which notes it serves. Your vault, your notes, your session — the default is simply what your vault already declares as your content (user_content_roots), at the granularity you wrote it (work/active/, not all of work/). Set mcp_exposed_roots in vault-manifest.json only if this vault holds material that is not yours to share — employer-confidential notes, a client's data. A note tagged private is never served, and memories are never served as ordinary notes, since they carry their own scope. One case reads across the two: when a capture's promoted: marker carries an anchor, recall serves that block out of brain/ so a foreign repo gets the corrected version rather than the capture as first written — bounded by this same policy, so a private or withheld note is refused there too.
Every read is logged with the calling repo, so "what did that session actually see" is answerable afterwards.
NOTE
Keeping vault material out of a public PR is the contract's job, not the exposure list's — a session can read your vault directly regardless. The prohibition injected into the calling session is the part that measurably holds.
Want the mechanics? ARCHITECTURE.md → Reaching the Vault From Another Repo walks the whole layer with diagrams: the four MCP surfaces and why the non-tool ones matter, the identity handshake, a search call traced end to end, how each memory's reach is evaluated, the write path, and every failure mode against the signal that reveals it.
📅 Daily Workflow
Morning: Run /om-standup. Your agent loads your North Star, active projects, open tasks, and recent changes. You get a structured summary and suggested priorities.
Throughout the day: Talk naturally. Mention a decision you made, an incident that happened, a 1:1 you just had, a win you want to remember. The classification hook nudges the agent to file each piece correctly. For bigger brain dumps, use /om-dump and narrate everything at once.
End of day: Say "wrap up" and the agent invokes /om-wrap-up — verifies notes, updates indexes, checks links, spots uncaptured wins.
Weekly: Run /om-weekly for cross-session synthesis — North Star alignment, patterns, uncaptured wins, and next-week priorities. Run /om-vault-audit to catch orphan notes, broken links, and stale content.
Review season: Run /om-review-brief manager and get a structured review prep document with all the evidence already linked.
🛠️ Commands
Defined in .claude/commands/. Run them in Claude Code, Codex CLI, or Gemini CLI.
| Command | What It Does |
|---|---|
/om-standup | Morning kickoff — loads context, reviews yesterday, surfaces tasks, suggests priorities |
/om-dump | Freeform capture — talk naturally about anything, routes it all to the right notes |
/om-wrap-up | Full session review — verify notes, indexes, links, suggest improvements |
/om-humanize | Voice-calibrated editing — makes Claude-drafted text sound like you wrote it |
/om-weekly | Weekly synthesis — cross-session patterns, North Star alignment, uncaptured wins |
/om-capture-1on1 | Capture a 1:1 meeting transcript into a structured vault note |
/om-incident-capture | Capture an incident from Slack/channels into structured notes |
/om-slack-scan | Deep scan Slack channels/DMs for evidence |
/om-peer-scan | Deep scan a peer's GitHub PRs for review prep |
/om-review-brief | Generate a review brief (manager or peer version) |
/om-self-review | Write your self-assessment for review season — projects, competencies, principles |
/om-review-peer | Write a peer review — projects, principles, performance summary |
/om-tidy | Self-maintenance — acts on every hygiene flag: archive, group, split. Never deletes, never commits |
/om-correct | Sweep a corrected fact — applies it at the single source, links restatements, preserves dated records |
/om-vault-audit | Audit indexes, links, orphans, stale context |
/om-vault-upgrade | Upgrade this vault through ShardMind (keeps your local improvements), or import content from an existing vault — version detection, classification, migration |
/om-prep-1on1 | Prep for an upcoming 1:1 — load person context, open items, suggested agenda |
/om-meeting | Prep for any meeting by topic — subject-forward briefing with open items and considerations |
/om-intake | Process meeting notes inbox — classify and route to the right vault notes |
/om-project-archive | Move a completed project from active/ to archive/, update indexes |
🤖 Subagents
Specialized agents that run in isolated context windows. They handle heavy operations without polluting your main conversation.
| Agent | Purpose | Invoked by |
|---|---|---|
brag-spotter | Finds uncaptured wins and competency gaps | /om-wrap-up, /om-weekly |
context-loader | Loads all vault context about a person, project, or concept | Direct |
cross-linker | Finds missing wikilinks, orphans, broken backlinks | /om-vault-audit |
people-profiler | Bulk creates/updates person notes from Slack profiles | /om-incident-capture |
review-prep | Aggregates all performance evidence for a review period | /om-review-brief |
slack-archaeologist | Full Slack reconstruction — every message, thread, profile | /om-incident-capture |
vault-librarian | Deep vault maintenance — orphans, broken links, stale notes | /om-vault-audit |
review-fact-checker | Verify every claim in a review draft against vault sources | /om-self-review, /om-review-peer |
vault-migrator | Classify, transform, and migrate content from a source vault | /om-vault-upgrade |
NOTE
Subagents are defined in.claude/agents/. You can add your own for domain-specific workflows.
📊 Performance Graph
The vault doubles as a performance tracking system:
Competency notes in
perf/competencies/define your org's competency framework — one note per competencyWork notes link to competencies in their
## Relatedsection, annotated with what was demonstratedBacklinks accumulate automatically — review prep becomes reading the backlinks panel on each competency note
Brag Doc aggregates wins per quarter with links to evidence notes
/om-peer-scandeep-scans a colleague's GitHub PRs and writes structured evidence toperf/evidence//om-review-briefgenerates a full review brief by aggregating everything: brag entries, decisions, incidents, competency evidence, and 1:1 feedback
TIP
To get started: create competency notes from the template, then link your work notes to them as you go. The graph does the rest.
📋 Bases
The bases/ folder contains database views that query your notes' frontmatter properties. They update automatically as notes change.
| Base | Shows |
|---|---|
| Work Dashboard | Active projects filtered by quarter, grouped by status — plus a Stale Actives view (active but untouched 14+ days) |
| Recently Touched | Every note by real modified time — the correct answer to "what did I work on recently" (filename dates lie for living notes) |
| Incidents | All incidents sorted by severity and date |
| People Directory | Everyone in org/people/ with role, team |
| 1:1 History | All 1:1 notes sortable by person and date |
| Review Evidence | PR scans and evidence grouped by person and cycle |
| Competency Map | Competencies with evidence counts from backlinks |
| Templates | Quick access to all templates |
Home.md embeds these views, making it the vault's dashboard.
📁 Vault Structure
Home.md Vault entry point — embedded Base views, quick links
AGENTS.md Operating manual — read by every agent, every session
vault-manifest.json Template metadata — version, structure, schemas
.shardmindignore Files excluded from `shardmind install` (CONTRIBUTING, translations, marketing media)
CHANGELOG.md Version history
CONTRIBUTING.md Template development checklist
README.md Product documentation
LICENSE MIT license
bases/ Dynamic database views (Work Dashboard, Incidents, People, etc.)
work/
active/ Current projects (1–3 files at a time)
archive/YYYY/ Completed work, organized by year
incidents/ Incident docs (main note + RCA + deep dive)
1-1/ 1:1 meeting notes — named <Person> YYYY-MM-DD.md
Index.md Map of Content for all work
org/
people/ One note per person — role, team, relationship, key moments
teams/ One note per team — members, scope, interactions
People & Context.md MOC for organizational knowledge
perf/
Brag Doc.md Running log of wins, linked to evidence
brag/ Quarterly brag notes (one per quarter)
competencies/ One note per competency (link targets)
evidence/ PR deep scans, data extracts for reviews
<cycle>/ Review cycle briefs and artifacts
brain/
North Star.md Goals and focus areas — read every session
Memories.md Index of memory topics
Key Decisions.md Significant decisions and their reasoning
Patterns.md Recurring patterns observed across work
Gotchas.md Things that have gone wrong and why
Skills.md Custom workflows and slash commands
reference/ Codebase knowledge, architecture maps, flow docs
thinking/ Scratchpad for drafts — promote findings, then delete
templates/ Obsidian templates with YAML frontmatter
.claude/
rules/ Claude Code-only guidance, loaded beside AGENTS.md
commands/ 20 slash commands
agents/ 10 subagents
scripts/ Hook scripts + charcount.ts utility
skills/ Obsidian + QMD skills, and the obsidian-mind mod
settings.json 5 hooks configuration
.scripts/ Vault-level tooling — QMD bootstrap (run once on a fresh clone)
.shardmind/ ShardMind sidecar — only used if installed via `shardmind install`
shard.yaml Manifest (name, version, modules, hooks)
shard-schema.yaml Wizard values + module gating
hooks/ bootstrap (git init + QMD), personalize (North Star), post-updateNOTE
.shardmind/is additive, not load-bearing. A clone-and-open vault never reads it; only theshardmindCLI does. Delete it and the vault keeps working. See the v6 layout contract in shardmind/docs/SHARD-LAYOUT.md.
📝 Templates
Templates with YAML frontmatter, each including a description field for progressive disclosure:
Work Note — date, description, project, status, quarter, tags
Decision Record — date, description, status (proposed/accepted/deprecated), owner, context
Thinking Note — date, description, context, tags (scratchpad — delete after promoting)
Competency Note — date, description, current-level, target-level, proficiency table
1:1 Note — date, person, key takeaways, action items, quotes
Incident Note — date, ticket, severity, role, timeline, root cause, impact
🔧 What's Included
🧩 Obsidian Skills
kepano/obsidian-skills pre-installed in .claude/skills/:
obsidian-markdown — Obsidian-flavored markdown (wikilinks, embeds, callouts, properties)
obsidian-cli — CLI commands for vault operations
obsidian-bases — Database-style
.basefilesjson-canvas — Visual
.canvasfile creationdefuddle — Web page to markdown extraction
🔍 QMD Skill
A custom skill in .claude/skills/qmd/ that teaches the agent to use QMD semantic search proactively — before reading files, before creating notes (to check for duplicates), and after creating notes (to find related content that should link to it).
🎨 Customize It
This is a starting point. Adapt it to how you work:
| What | Where |
|---|---|
| Your goals | brain/North Star.md — grounds every session |
| Your org | org/ — add your manager, team, key collaborators |
| Your competencies | perf/competencies/ — match your org's framework |
| Your tools | .claude/commands/ — edit for your GitHub org, Slack workspace |
| Your conventions | AGENTS.md — the operating manual, evolve it as you go (.claude/rules/ for Claude Code-only additions) |
| Your domain | Add folders, subagents in .claude/agents/, or classification rules in .claude/scripts/ |
IMPORTANT
AGENTS.mdis the operating manual. When you change conventions, update it — your agent reads it every session.
🔄 Upgrading
Ask your agent
The easiest way — just tell your agent:
Update this vault to the latest obsidian-mind from https://github.com/breferrari/obsidian-mindThe agent will pull the latest changes, resolve conflicts, and update infrastructure files. Works with Claude Code, Codex CLI, or Gemini CLI.
Updating an existing clone
If you cloned the repo directly:
cd your-vault
git pull origin mainNew files appear automatically and hook scripts are updated in place. If you edited CLAUDE.md, the pull stops on it, because you changed a file the template deleted: move your additions out as Troubleshooting describes, then delete it.
Updating a fork
If you forked the repo:
git remote add upstream https://github.com/breferrari/obsidian-mind.git
git fetch upstream
git merge upstream/mainResolve any conflicts in files you customized (typically AGENTS.md, brain/ notes; a CLAUDE.md you edited conflicts with its deletion, see Troubleshooting). Infrastructure files (.claude/scripts/, .codex/, .gemini/) should merge cleanly.
Adopting an existing clone into ShardMind (v5.x → v6)
Already cloned obsidian-mind and want the wizard, optional modules, and three-way-merge upgrades without losing your customizations? shardmind adopt reconciles your existing vault into a managed v6 install — keeping every byte of your edits and only adding the .shardmind/ sidecar + shard-values.yaml:
npm install -g shardmind
shardmind adopt github:breferrari/obsidian-mindThe 2-way diff UI walks you through any local changes, asks per-file what to keep, then writes the engine metadata. Result: a v6-managed vault with your existing content intact, ready for shardmind update from there forward. No re-cloning.
Migrating from an older vault (or any other vault)
Using a pre-v5 obsidian-mind, or migrating from a totally different Obsidian vault? The /om-vault-upgrade command migrates your content into the latest template:
# 1. Clone the latest obsidian-mind
git clone https://github.com/breferrari/obsidian-mind.git ~/new-vault
# 2. Open it in your agent
cd ~/new-vault && claude # or codex, or gemini
# 3. Run the upgrade pointing to your old vault
/om-vault-upgrade ~/my-old-vaultThe agent will:
Detect your vault version (v1–v3.x, or identify it as a non-obsidian-mind vault)
Inventory every file — classify as user content, scaffold, infrastructure, or uncategorized
Present a migration plan — you see exactly what will be copied, transformed, and skipped
Execute after your approval — transforms frontmatter, fixes wikilinks, rebuilds indexes
Validate — checks for orphans, broken links, missing frontmatter
Your old vault is never modified. Use --dry-run to preview the plan without executing.
NOTE
Works with any Obsidian vault, not just obsidian-mind. For non-obsidian-mind vaults, the agent reads each note and classifies it semantically — routing work notes, people, incidents, 1:1s, and decisions to the right folders.
🩺 Troubleshooting
Claude Code ignores the vault's conventions. Claude Code reads a CLAUDE.md in place of AGENTS.md and skips AGENTS.md without saying so. Two ways that happens:
A leftover
CLAUDE.mdin the vault. An update keeps aCLAUDE.mdyou edited. The session hygiene flags sayCLAUDE.md present beside AGENTS.md, and/om-vault-upgradelists it under Move out. Move what you added to.claude/rules/local.md(or intoAGENTS.mdif every agent needs it, keepingAGENTS.mdunder 23 KB, where the hygiene flags start: Antigravity stops reading it at ~24,000 bytes), then deleteCLAUDE.md. Nothing is merged automatically.A
CLAUDE.mdin a folder above the vault. Claude Code reads that one too, andAGENTS.mdis skipped. The hygiene flag cannot see it, because it checks the vault root only. Move the vault out from under that folder, or move that file's content somewhere it applies only where you want it.
Gemini CLI loads no instructions when started in a vault subfolder. Gemini's context search walks up the folders, but its settings lookup does not, so a session started in a subfolder never reads the vault's .gemini/settings.json and never learns to look for AGENTS.md. Start Gemini from the vault root.
Gemini ignores what you added to GEMINI.md. Gemini now reads AGENTS.md (through .gemini/settings.json), so a leftover GEMINI.md is never loaded. The hygiene flags say GEMINI.md present beside AGENTS.md, and /om-vault-upgrade lists it under Move out. Move your additions to AGENTS.md, then delete it.
Grok reads nothing from the vault. Grok loads project instructions only from a trusted folder. Trust the vault once: run /hooks-trust in a Grok session, or launch with --trust. One grant covers MCP, LSP, hooks, project instructions and project skills.
🗺️ Roadmap
Issues and bug reports welcome. Code contributions go through assignment: comment on an issue to ask for it, and open a PR once it's assigned to you. A PR for an issue not assigned to you is closed without review. See CONTRIBUTING.md.
🙏 Design Influences
kepano/obsidian-skills — Official Obsidian agent skills
James Bedford — Vault structure philosophy, separation of AI-generated content
arscontexta — Progressive disclosure via description fields, session hooks
👤 Author
Created by Brenno Ferrari — Senior iOS Engineer in Berlin, building developer tools with Claude Code.
📄 License
MIT
ARCHITECTURE.md
Architecture
This document describes how obsidian-mind fits together — the load-bearing seams, the design choices behind them, and where to extend the system. It is aimed at contributors and anyone forking the template who wants to customize it without breaking the mechanics.
It is not a folder tour. For the day-to-day layout, read AGENTS.md. For the user-facing story, read README.md.
System Overview
obsidian-mind is a plain Obsidian vault with four systems layered on top:
The vault itself — Markdown files, frontmatter, wikilinks. Portable, git-tracked, Obsidian-browsable.
A hook pipeline — small TypeScript scripts invoked by the agent harness at lifecycle events (session start, every message, after writes, before compaction, after each response; Gemini at session end).
A semantic search layer (QMD) — a separate CLI + SQLite index + MCP server, all scoped to a named index read from
vault-manifest.json.The
omMCP server — the vault as a service, so a session running in a different repository can search it, read notes, follow the graph, and record back into it.
The layers communicate through one coordination point: vault-manifest.json. It declares the template version, the QMD index name, which folders the server serves, where memories live, and the boundary between infrastructure files (shipped by the template) and user content (created by the human).
The fourth layer is the one that changes the shape of the system. Layers 1–3 assume the agent is sitting in the vault; om removes that assumption, which is why identity — "which repo is asking?" — becomes a first-class concept below.
flowchart TB
User["Human operator"]
subgraph Agent["Agent session INSIDE the vault"]
Harness["Agent harness"]
Hooks["Hook pipeline<br/>(.claude/scripts/)"]
end
subgraph Foreign["Agent session in ANOTHER repo"]
FSession["Coding session"]
FConfig[".mcp.json entry<br/>+ that repo's own instruction file"]
end
subgraph Vault["Obsidian vault"]
Manifest["vault-manifest.json<br/>(source of truth)"]
Notes["Markdown notes<br/>(work/, perf/, brain/, org/)"]
Memories["memories/<br/>(cross-repo, scope declared per memory)"]
Config[".claude/ .codex/ .gemini/<br/>(hook configs)"]
end
OM["om MCP server<br/>(om-mcp.ts)"]
QMD["QMD<br/>(named SQLite index + embeddings)"]
Obsidian["Obsidian app<br/>(graph, Bases, CLI)"]
User <--> Agent
User <--> Foreign
User <--> Obsidian
Harness --> Hooks
Harness -->|"mcp__qmd__*"| QMD
Hooks --> Manifest
Hooks --> Notes
Hooks --> QMD
Config --> Hooks
FConfig --> FSession
FSession <-->|"MCP over stdio"| OM
OM --> Manifest
OM --> Notes
OM --> Memories
OM -->|"MCP client"| QMD
QMD --> Notes
QMD --> Memories
Obsidian --> NotesThe vault is the persistent state. Everything else is machinery around it.
Division of Responsibility
Two actors do the work, and the boundary between them is the most important design choice in the system.
Procedural code owns the environment. Hooks in .claude/scripts/ classify messages, validate writes, maintain the QMD index, inject context at session start, and back up transcripts before compaction. None of this logic is in the agent's head. It runs identically whether the agent is Claude Code, Codex, or Gemini, and it produces deterministic, testable behavior — every contract in .claude/scripts/lib/ is locked by a unit suite, run in CI on every push.
The agent owns content. Writing notes, choosing where to file them, adding wikilinks, updating indexes, promoting thinking drafts, drafting review briefs — these are judgments, not rules, and they live with the agent. AGENTS.md documents the conventions the agent should follow; it does not replace the agent's judgment.
The two halves meet at small, well-defined handoffs: hooks inject context and routing hints through stdout, the agent reads the vault and calls Write or Edit. Neither side reaches across the boundary. This is what keeps the hooks portable (no agent-specific logic) and keeps the agent's tokens pointed at judgment rather than bookkeeping.
Design Principles
Four ideas shape every decision in this template. When a change breaks one of them, it needs a very good reason.
1. Graph-first, not folder-first
Folders group by purpose. Links group by meaning. A note lives in one folder (its home) but links to many notes (its context). Competency notes stay definitional and receive evidence through backlinks — review prep becomes reading the backlinks panel on each competency. This is why every new note must link to at least one existing note, and why the agent is instructed to treat orphan notes as bugs.
2. Vault-first memory
All durable knowledge lives in the vault, inside brain/ topic notes. The agent-specific memory indexes (~/.claude/.../MEMORY.md) are pointers to vault locations, never the storage themselves. This keeps memory git-tracked, machine-portable, and visible in the Obsidian graph.
The om server extends this outward rather than around it. A session in another repo writes into memories/ — still Markdown, still in the graph, still yours to open in Obsidian — instead of into a private store belonging to some memory service. A generic memory server solves the wrong half of the problem: it gives you a second knowledge base that is not your vault.
Because those memories are written from many repos and read by many repos, each one declares its reach when written — which projects and platforms it applies to. A reader never widens what a writer declared. That is a relevance rule, not an access-control rule: the store is the user's, the sessions are the user's, and the question being answered is "which lessons bear on the repo asking?" — not "who is allowed to know this?"
3. Progressive disclosure
SessionStart injects a small block of lightweight context (North Star excerpt, git summary, tasks, file listing). Full note contents are pulled on demand via QMD semantic search. A full file read is a last resort, not a default.
Session cost stays flat regardless of vault size because it is enforced, not merely intended. Two of those inputs grow with the vault — the file listing grows with every note, the North Star excerpt with every status edit — so without a ceiling the eager layer drifts upward a little every day and nobody notices until a session is paying for it. A byte budget holds the total; over it, the cheapest-to-lose sections degrade to pointers, worst-priority first, and the closing size meter names each one it dropped. Line-based caps cannot do this job: shortening entries under a line cap just slides the window deeper and refills it. Budget and listing-collapse threshold are set in vault-manifest.json.
The budget has a hard ceiling it cannot be raised past: Claude Code's hook output cap. Claude Code caps a hook's context at 10,000 characters, with no setting to raise it. Over the cap, the session gets a file path and the first 2,000 characters, and Claude Code does not ask the model to read the rest. A budget above the cap never binds: nothing degrades, the meter on the last line is the first thing lost, and the meter the user reads still looks healthy (#254). So session-start.ts clamps any budget, set or unset, to the cap less the JSON envelope and room for the meter (about 9,000; a section is sized as the larger of its UTF-8 bytes and its JSON-encoded length, so this holds the character cap too). When it clamps a configured value, the meter says so. The sections that never degrade (the date, open tasks, hygiene) could still pass the cap on their own, so the whole output is fitted as a backstop: cut on a line, marked where it was cut, the meter still last and saying it was "truncated to fit the hook output cap".
Within that ceiling, the budget is a runaway guard, not a squeeze. The meter is the detector; the budget is the emergency brake. A ceiling low enough to bite in normal use degrades your context every session instead of catching a problem. If the budget starts firing, look at what grew. It can no longer be raised past the cap, so the fix is to make the section smaller or let it degrade to its pointer. Content a session must always have, in full, belongs in an instruction file (AGENTS.md, plus .claude/rules/ on Claude Code), which this cap does not apply to.
The same asymmetry decides what may degrade. Rank the eager layer by value density, not size: filenames are the cheapest bytes (one Glob rebuilds them), so the listing surrenders first. Anything irreplaceable — identity, personal context, correctness guards — carries no fallback and is never traded for plumbing. Optimizing this layer means removing duplication, not information, which is exactly why re-entry via resume/compact drops the static bulk: it is already in the conversation, so omitting it loses nothing.
4. Agent-agnostic core
The hook scripts, subagent prompts, command definitions, and vault conventions are pure Markdown and TypeScript with no SDK dependencies. Each agent (Claude Code, Codex CLI, Gemini CLI) brings its own config file pointing at the same scripts. Only the ~/.claude/ auto-memory loader is Claude Code-specific.
The Manifest as Source of Truth
vault-manifest.json is the one file that every layer reads. It answers eight questions:
| Question | Field |
|---|---|
| What version of the template is this? | version, released, version_fingerprints |
| What does QMD call its store? | qmd_index, qmd_context, qmd_min_version |
| Which files are template infrastructure? | infrastructure[] |
| Which files are user content? | user_content_roots[], scaffold{} |
| What frontmatter is required for each note type? | frontmatter_required{} |
Which notes does the om server serve, and where do memories live? | mcp_exposed_roots[], mcp_never_expose[], memory_root, mcp_inbox |
| How much context may the eager layer spend? | eager_layer_budget_bytes (as hook output, held under the 10,000-char hook cap), eager_layer_instruction_budget_bytes (when the mod delivers it as an instruction file), listing_collapse_threshold |
Which model does a reason spawn run on? | reason.model — unset means the user's own CLI default |
The qmd_index field is the most load-bearing. Five independent callers read it, and they fail silently when they disagree — one writes to a store another never reads, which surfaces only as "0 documents" or as an empty search:
flowchart LR
Manifest["vault-manifest.json<br/>qmd_index, or the folder slug"]
Boot["qmd-bootstrap.ts<br/>(one-time setup)"]
SessionStart["SessionStart hook<br/>(session-start.ts)"]
MCP[".mcp.json wrapper<br/>(qmd-mcp.mjs)"]
Refresh["Mid-session refresh<br/>PostToolUse / Stop / PreCompact<br/>→ qmd-refresh-run.ts worker"]
OM["om MCP server<br/>(mcp-context.ts)"]
Store[("QMD SQLite store<br/>named per vault")]
Manifest --> Boot
Manifest --> SessionStart
Manifest --> MCP
Manifest --> Refresh
Manifest --> OM
Boot -->|"create + first index"| Store
SessionStart -->|"re-index at startup"| Store
Refresh -->|"debounced refresh"| Store
MCP -->|"search tools"| Store
OM -->|"search, and re-index after a write"| StoreAll five resolve the name through one function, resolveQmdIndex — the qmd_index field when pinned, otherwise the vault folder name slugified — so the vault coexists with other QMD-indexed projects on the same machine without collision. The template ships the field empty so two installs never share a store by default. Set qmd_index and the next bootstrap creates a fresh, isolated store under that name.
Routing every caller through one resolver is the fix for a real class of failure rather than a tidiness preference: a hardcoded or independently-derived name in any one of the five produces a vault that indexes into one store and searches another, with nothing anywhere reporting an error. health on the om server checks that the index it would query actually belongs to this vault.
The infrastructure[] vs user_content_roots[] split is what makes /om-vault-upgrade work. When importing from an older template, the migrator overwrites infrastructure files wholesale and preserves user content untouched.
Lifecycle Hooks
Five hooks run at different moments in a session. Each is a small Node script invoked via --experimental-strip-types (TypeScript executes directly, no build step).
sequenceDiagram
participant User
participant Agent
participant Hooks
participant Vault
participant QMD
User->>Agent: start session
Agent->>Hooks: SessionStart
Hooks->>Vault: read North Star, git log, tasks, file listing
Hooks->>QMD: re-index (async)
Hooks-->>Agent: inject a briefing, held under the byte budget
loop each user message
User->>Agent: prompt
Agent->>Hooks: UserPromptSubmit
Hooks->>Hooks: classify (decision, incident, win, 1:1, ...)
Hooks-->>Agent: routing hints + any Stop report saved last turn
end
loop each Write/Edit to .md
Agent->>Hooks: PostToolUse
Hooks->>Hooks: validate frontmatter + wikilinks
Hooks->>QMD: debounced refresh (detached)
Hooks-->>Agent: warnings if invalid
end
Note over Agent,Hooks: if context fills
Agent->>Hooks: PreCompact
Hooks->>Vault: back up transcript to thinking/session-logs/
loop each completed response
Agent->>Hooks: Stop
Hooks->>QMD: debounced refresh (detached)
Hooks-->>User: short summary, one line per section, first time this session or when changed
Hooks->>Hooks: save the full report for the next prompt
endA few specific design choices are worth calling out:
SessionStartinjects, it does not load. It builds a briefing (filename listing, North Star excerpt, git summary, open tasks aggregated fromwork/active/and the vault root) and hands it to the agent. Full note contents never flow through this hook. Its size is bounded byeager_layer_budget_bytes, held under Claude Code's 10,000-character hook output cap, and reported by the meter on the last line of every injection, so the cost is visible rather than assumed. When the template's Claude Code mod delivers the same briefing as an instruction file instead, the hook stands down and the briefing is bounded byeager_layer_instruction_budget_bytes, since instruction files are not under the hook cap (#264). The same backstop holds there, in bytes: sections that never degrade are cut at the budget plus the meter's room, and the meter says "truncated to fit the instruction budget". On the hook path the briefing goes out as one JSON object,{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":…}}, for every agent (#340). Gemini parses a hook's stdout as JSON and takes context only from that field (per its hooks reference as of 2026-10-13, not observed), and Claude Code accepts the same shape, capping theadditionalContextstring rather than the raw stdout. Codex takes context from itsadditionalContexttoo (observed with Codex CLI 0.160.0 as of 2026-10-13, #345). Whether Grok does is unverified (TBC). So the budget and the backstop measure the JSON-encoded text (escaping counts), keeping the whole stdout under the cap for an agent whose cap is undocumented. The mod'sdeliverrun stays plain text, since it becomes an instruction file. The open-tasks scan is filesystem-only so the hook never spawns the Obsidian CLI — that subprocess flashes the Electron app on macOS when no instance is running (#83).UserPromptSubmitclassifies, it does not route. It tags the prompt with hints likeARCHITECTURE discussionorDECISION; the agent decides where to file. Keeping the hook opinion-free means the routing logic lives inAGENTS.md, which is editable per-user without touching scripts.QMD refresh is shared, debounced, and detached. Three hook entries fire the same refresh helper —
PostToolUse(after.mdwrites),PreCompact(before transcript backup; writes tend to cluster before compaction), andStop(after a response) — sharing one sentinel file so a burst of events produces at most one worker per debounce window. The actual indexing runs in.claude/scripts/qmd-refresh-run.tsas a detached, stdio-silent worker (qmd update→qmd embed→ tail-chaseqmd update), so the parent hook returns in milliseconds and nothing flows to the agent's context.VAULT_QMD_REFRESH=offswitches the detached refresh off (exactlyoff, as wiki-mind's copy reads it): noqmd-refresh-run.tsworker, and no detachedqmd updateor bootstrap fromSessionStart. The debounce decision and its sentinel stamps still run. The test suite sets it in every test process and every hook a test runs. The test runner also stops and fails on any surviving worker whose command line names the run's temp folder, which every trailing worker's does; an immediate-path worker carries no path, so for those the switch is the guard (#337).PreCompactalso backs up the transcript. In addition to kicking the QMD refresh, it copies the current session transcript out tothinking/session-logs/so long conversations remain recoverable after compaction.Stopreports once, not every turn. Claude and Codex fire it whenever the agent finishes a response, not when the session ends (#252). It triggers the shared refresh every time, but shows the checklist and hygiene findings only the first time a session sees that exact report, and again when the findings change; otherwise it returns the empty JSON envelope. A warning that repeats unchanged is one users learn to ignore (#155). The dedupe reuses the classifier's per-session state shape (#107): one self-pruning file, failing open to "show" on a missingsession_idor unreadable state.A changed
Stopreport reaches the agent too (#256). AsystemMessageis shown to the user and never added to the model's context, so the drift the agent is best placed to fix never reached it. And everyStopoutput that does reach the model is printed in full for the user: adecision: "block"reason under a "Stop hook error" label (v8.5.0 shipped that),StopadditionalContextunder "Stop hook feedback".UserPromptSubmitadditionalContextis the one channel that reaches the model and is never shown. So when the findings change,Stopshows the user a one-line-per-section summary and saves the full report for the session (lib/stop-handoff.ts: written atomically, claimed by rename so it is delivered once, the newest report wins, a week-old one is pruned). The next prompt'sclassify-messagetakes it and hands it to the agent beside any routing hints, with a preface telling it to deal with the user's message first. If the report cannot be saved,Stopsends it as feedback instead (Claude Code; Codex's handling of Stop feedback is unverified): visible, but never lost.SessionEndand aStopwithout asession_idhave no next prompt to ride, so the user gets the full report.Why not
SessionEnd. It looks like the natural home for an end-of-session checklist, but Claude Code discards a SessionEnd hook'ssystemMessage, and Codex documents SessionEnd as advisory and does not surface itssystemMessage. Wired there, the checklist would reach nobody on two of the three agents. Gemini does display it during shutdown, so Gemini keeps its SessionEnd wiring. The acting path is/om-wrap-up, which runs the hygiene pass while tools are still live.
The Claude Code mod
Claude Code 2.1.287 added mods: plugins whose code runs inside Claude Code and can wrap the settings-hook events as classic.<Event> (PreToolUse rides tool.call instead). The template ships one, obsidian-mind, at .claude/skills/obsidian-mind/; Claude Code loads it in place once the vault folder is trusted, from the session's working folder only.
The hooks stay the engine. Codex and Gemini have no mods, and on Claude Code the mod does not load everywhere (an older CLI, an untrusted folder, a session started in a subfolder, managed-only policy). So the mod adds no behaviour of its own: it runs the same scripts and only changes the channel their output reaches the session through.
The switch rides on the event. What the mod passes to
next()is what the settings hooks read on stdin (lib/om-mod.ts). For an event it handles, the mod does its own work first and then passesom_mod: "standdown", and the script exits. The flag exists only on events the mod's hook actually ran on: a mod that throws, crashed or never loaded sends the original event, and the hook runs as without it. An environment variable would outlive a dead mod and leave the hooks silent; answering the event withoutnextwould silence the user's own hooks too.SessionStart becomes an instruction file. The mod runs
session-start.tswithom_mod: "deliver"(the full layer, held toeager_layer_instruction_budget_bytes) and adds the output inprompt.contextas a project instruction file. Claude Code re-reads instruction files after compaction and/clear, gives them to general-purpose subagents, and does not cut them at the hook limit. The text is kept in the mod's per-session state ($.state, which survives a hot reload of the module) and cleared (and redrawn) before every run that starts a conversation, so a run that fails there cannot leave an older context riding beside the full layer the settings hook then prints. A compaction keeps it: the hook's fallback there is only a pointer to a static half that, under the mod, was never in the conversation, so the last good context is the better thing to keep. It is also written to.claude/session-context.md(gitignored), which is where/memorypoints: unawaited, so delivery never waits on the file. The file is only what/memoryshows: two runs seconds apart could in principle leave the older text on disk, and that case is accepted rather than guarded, since runs are minutes apart in practice.Stop becomes a line under the answer. The mod runs
stop-checklist.tswithom_mod: "report", which returns the report as data and claims no state; the mod keeps which report each session was last given, and whether it reached the agent, by session id, in$.store(a file Claude Code keeps per plugin, capped at the most recent sessions). Aclaude --resumein a new process then neither repeats a report the agent had nor loses one still waiting, matching the settings hook's file-backed handoff. A report counts as delivered when a turn starts with the prompt that carried it; that turn can start inside the prompt's ownprompt.submit(observed on 2.1.288), so the prompt is recorded as carrying it before the hook callsnext. What is waiting to be delivered is one record in$.state: the report, its line and its urgent finding, and the session it is for. When the findings change it draws one line under the next main-loop answer that completes (turn.complete; a subagent turn, an interrupt or an error keeps the line pending), after any line a hook below already set, and attaches the full report to the next prompt the person sends (prompt.submitcontext, never shown). Prompts from peers, notifications, schedules and other plugins do not carry it, and a prompt dropped below keeps it queued. A prompt enters when it is queued, not when its turn starts, and a queued prompt can be pulled back out of the queue unrun; so the report a prompt took is held until a turn starts with that prompt (or a folded turn that contains it), and if another prompt's turn starts first, the queue being first in, first out, it goes back to be delivered with the next prompt. A duplicate delivery is possible there; a lost one is not. A report markedurgentis submitted fromturn.completeas a prompt of its own, framed as the plugin's rather than the person's. The finding also rides inside the queued report, so it reaches the agent whether or not its own turn happens, and one such turn runs per prompt the person sends that actually entered: findings that keep changing while the agent fixes them cannot chain turns. Once a prompt enters with the report, a line or urgent finding still waiting is dropped, unless a newer report was queued while it entered. Every session start except a compaction (startup,/clear, an in-process resume or fork) drops anything queued, since$.stateis not reset there; a compaction continues the conversation and keeps it. A dropped report never reached the agent, so its session forgets having been shown it and the same findings show again; a report the agent already had stays shown. Each such start also bumps a generation counter, and a prompt that took the report and then failed to enter puts it back only if no such start happened meanwhile, so a report in flight across a/clearcannot land in the new conversation. State that may have changed while a hook awaitednextis read throughupdate, neverread: every$.stateread in one dispatch sees one moment. The report the mod receives carries its own framing (MOD_PREFACE), since the user saw a line rather than a summary and the prompt may be the plugin's. The submit never comes fromclassic.Stop: Claude Code refuses it there, because it would wait on the turn the hook may be holding. The template's report has no urgent class; the field is there for a vault that adds one.The work happens before
next. If the script fails, the mod's hook throws before passing the flag down, Claude Code skips it, and the settings hook delivers as usual. A failure afternextresolved would keep the stood-down result, so nothing runs after it.
QMD Integration
QMD provides semantic search. It is the mechanism behind most of the agent's retrieval intelligence. Four entry points all read from the same named index:
| Caller | Entry point | When |
|---|---|---|
| Agent tool menu | .mcp.json → qmd-mcp.mjs | Every mcp__qmd__* tool call from a session inside the vault |
| Session startup | session-start.ts | Re-index on every new session |
| Mid-session refresh | validate-write.ts / stop-checklist.ts / pre-compact.ts → lib/qmd-refresh.ts (shared sentinel + debounce) → detached qmd-refresh-run.ts | After .md writes, after responses, and before compaction |
The om server | lib/mcp-qmd-client.ts (search) and reindexSync (after a write) | Every search, every queried recall, every remember / record_work |
Every qmd update invocation re-reads the per-index YAML (~/.config/qmd/<index>.yml), so changes to the collection config — including the ignore list synced from .obsidian/app.json — propagate to every surface without a session restart.
What QMD actually runs
Worth understanding, because it explains the cost profile of everything built on top. QMD runs three small models locally. There is no API key, no per-query cost, and it works offline.
| model | size | job |
|---|---|---|
Qwen3-Embedding-0.6B | ~640 MB | turns notes and queries into vectors |
qmd-query-expansion-1.7B | ~1.28 GB | rewrites a query into better search terms, and writes hypothetical answers for HyDE — reached by the qmd query CLI, not by the typed sub-queries this template sends (see below) |
Qwen3-Reranker-0.6B | ~640 MB | reorders the shortlist by actual relevance |
The embedder is chosen, not inherited. qmd defaults to embeddinggemma-300M, and the bootstrap replaces it. Measured on a 684-document vault against 211 human-labelled retrieval pairs, query path otherwise untouched: rank-1 0.299 → 0.469 and found@5 0.521 → 0.758 (McNemar exact p < 0.0001), replicated on a second independently written vault at rank-1 0.650 → 0.950 (p = 0.0003). It costs ~13ms a query and roughly doubles a full index build. Reranking is not a cheaper substitute for it — on the default embedder reranking is a large significant gain, on this one it is not significant on rank-1, because it had been compensating for the embedder; this model with no reranker beats the default with one, at an eighth of the latency.
Set models.embed in the qmd config yourself and the bootstrap leaves it alone — it replaces qmd's default, never a choice. Changing the model invalidates every existing vector, since the two produce different dimensions and qmd refuses to mix them, so the bootstrap forces a full re-embed in the same run.
They download on first use and are cached. QMD offloads to the GPU when it finds one — CUDA on a discrete card, Metal on Apple Silicon — and falls back to CPU otherwise. qmd doctor reports which.
The CLI verbs map onto that stack, cheapest first:
| verb | what runs | cost |
|---|---|---|
qmd search | BM25 keyword matching, no model at all | instant |
qmd vsearch | vector search — embeds the query, then compares | one embed |
qmd query | the full hybrid: sub-queries, then rerank | embed + optional expansion + rerank |
qmd update | parse and index note text | no model |
qmd embed | build the vectors for new or changed notes | the embedding model, per note |
Two consequences shape the design of everything downstream:
updateandembedanswer different questions.updatedecides whether a note is findable at all;embeddecides only where it ranks. They are separated everywhere in this template, and the split is why a write can report success honestly without waiting on a local model run.Reads are the expensive side, not writes. A query has to embed before it can search, so a semantic lookup costs a model pass while a keyword lookup costs none. This is the opposite of the usual database intuition, and it is why the retrieval paths below are careful about when they pay for a vector.
flowchart LR
subgraph Text["Text path — no model"]
Update["qmd update"]
FTS[("BM25 / FTS index")]
Update --> FTS
end
subgraph Vector["Vector path — local models"]
Embed["qmd embed"]
Emb["embeddinggemma-300M"]
Vec[("vector index")]
Embed --> Emb --> Vec
end
subgraph Read["Query time"]
Q["qmd query"]
Expand["qmd-query-expansion-1.7B<br/>expansion + HyDE"]
Rank["Qwen3-Reranker-0.6B"]
Q --> Expand
Q --> Rank
end
FTS --> Q
Vec --> Q
Notes["Markdown notes"] --> Update
Notes --> Embedflowchart TB
subgraph Reader["Agent tools"]
MCP_get["mcp__qmd__get"]
MCP_query["mcp__qmd__query"]
MCP_multi["mcp__qmd__multi_get"]
MCP_status["mcp__qmd__status"]
end
subgraph Writer["Index maintenance"]
Boot["qmd-bootstrap.ts<br/>(one-time)"]
Session["session-start.ts<br/>(per session)"]
Refresh["qmd-refresh-run.ts<br/>(per write/stop/pre-compact,<br/>debounced, detached)"]
end
Wrapper["qmd-mcp.mjs<br/>(reads qmd_index from manifest)"]
CLI["qmd CLI<br/>(--index <name> from manifest)"]
Store[("Named SQLite store<br/>+ embeddings")]
MCP_get --> Wrapper
MCP_query --> Wrapper
MCP_multi --> Wrapper
MCP_status --> Wrapper
Wrapper --> Store
Boot --> CLI
Session --> CLI
Refresh --> CLI
CLI --> StoreQMD is technically optional. When it isn't installed at all, the agent falls back through a preference order defined in AGENTS.md and the qmd skill: MCP tools first when registered, then the qmd CLI, then Grep/Glob/Read as a last resort. Every fallback step is non-fatal: .mcp.json entries that fail to launch are skipped with a harmless warning, the hook scripts detect a missing qmd binary and no-op, and the operating manual tells the agent what to reach for next.
QMD over MCP — the in-vault retrieval path
This section is about the
qmdMCP server, which a session inside the vault uses. Theomserver, which a session in another repo uses, is a different thing built on top of it — see Reaching the Vault From Another Repo. The one rule connecting them: a consuming repo registersom, never rawqmd.
When the MCP server is registered, the agent's normal path to QMD is through typed tools — mcp__qmd__query, mcp__qmd__get, mcp__qmd__multi_get, and mcp__qmd__status — that appear in its tool menu alongside Read and Edit. These come from the Model Context Protocol server declared in .mcp.json, launched by a thin wrapper (.claude/scripts/qmd-mcp.mjs) that reads qmd_index from the manifest and invokes qmd mcp underneath. The CLI remains available — and is documented as the fallback — but during a session with MCP live, the agent goes through typed tools, not shell.
The wrapper exists so that each vault can run its own isolated index without users hand-editing .mcp.json. The index name flows from the manifest into the wrapper, the wrapper into QMD, QMD into its per-vault SQLite store.
The contract matters because the alternative — teaching every subagent and slash command to shell out to qmd search on every call — would couple each prompt to QMD's CLI surface, duplicate parse/retry logic across files, and force every prompt to re-explain the tool. MCP collapses all of that into one typed interface for the in-session path. When QMD changes its CLI, the wrapper adapts; the rest of the template is insulated. When another MCP-aware service needs to join the vault (a bug tracker, a docs search, a calendar), it registers in .mcp.json and gains the same privileged position.
One ignore list, two engines
Obsidian and QMD both need to know which files to hide from search. Rather than maintain two lists that can drift, the template treats .obsidian/app.json → userIgnoreFilters as the single source of truth. qmd-bootstrap.ts reads that array and writes it into the QMD per-index YAML (~/.config/qmd/<index>.yml) as the collection's ignore field. Every subsequent qmd update (initial index, mid-session refresh, session-start reindex) honors the list.
flowchart LR
App[".obsidian/app.json<br/>userIgnoreFilters"]
Boot["qmd-bootstrap.ts"]
YAML["~/.config/qmd/<index>.yml<br/>collections.<name>.ignore"]
Obs["Obsidian<br/>(search, graph, switcher)"]
QmdUpdate["qmd update<br/>(every invocation)"]
App --> Obs
App --> Boot
Boot --> YAML
YAML --> QmdUpdateThis is why the list lives in Obsidian's config and not vault-manifest.json: users who adjust what's hidden in Obsidian's UI get the same change propagated to QMD on the next bootstrap. Files that are infrastructure (template dev docs like this one, CHANGELOG.md, CONTRIBUTING.md) are good candidates; user-authored content should not be here.
Reaching the Vault From Another Repo
Everything above assumes the agent is running inside the vault. The om MCP server removes that assumption.
The problem, precisely
A session working in your app's repo has no durable memory of why anything was decided. It re-derives context every time, asks you, or guesses. The knowledge exists — it is in your vault — but the session is in a different directory with no way to reach it.
Bolting a generic memory server onto that session solves the wrong half. Those write to their own store, in their own shape, and you end up with a second knowledge base that is not your vault, not in your graph, and not yours to browse. om exposes the vault itself.
An MCP server is four surfaces, not one
The non-tool surfaces turned out to matter more than expected, so it is worth naming all four and which direction each runs.
| surface | direction | what it is | who triggers it |
|---|---|---|---|
instructions | vault → session | the vault's rules, injected into the calling session's system prompt at connect | automatic, once per connection |
tools | session → vault | search, expand, recall, remember, record_work, reason, health | the model decides |
resources | session → vault | notes listable and readable by vault://note/<path> URI | the model or the client |
prompts | you → vault | recall_topic, prior_art — slash commands in the calling session | the human decides |
The split between rows 2 and 4 is load-bearing, because of an asymmetry measured while building this:
Prohibitions propagate. Routing instructions do not.
A rule in instructions — "never put a session URL in a commit" — held under direct pressure and under an authority override. A positive instruction — "consult the vault before answering questions about past decisions" — is advisory and gets skipped whenever a nearer source exists. A server can stop a session doing something; it cannot make one go looking.
That is why prompts exist (the human invokes them, so no model decision is involved) and why the install requires a pointer in the consuming repo's own instruction file (CLAUDE.md or AGENTS.md) — the nearest source wins, so the nearest source has to be the thing that says "go look."
The tier ladder
| tier | what | what it costs you at call time |
|---|---|---|
| 0 — presence | the contract reaches the calling session | one-off, no extra session spawned |
| 1 — retrieval | search, expand, resources | seconds, no model call beyond the local embedding |
| 2 — capture | recall, remember, record_work | no model call |
| 3 — reasoning | reason — a spawned session reads the vault and judges | seconds to minutes of waiting |
Tier 0 is the counterintuitive one. The original design assumed the vault would have to think for you: spawn its own session, hand back an answer. That works and it is expensive. Tier 0 instead hands the calling session the vault's contract and lets it think with the model you are already using.
The ladder is about latency, not money. Tier 3 runs your CLI, on your machine, under your auth — the same thing you get typing claude. Prefer search when search would do because it answers in two seconds, not because a second session is a hazard.
Module map
The entry script is deliberately thin. It owns only what a test cannot usefully drive — resolving the vault, opening stdio, holding the lazy qmd child. Every decision worth arguing about lives in a library module that can be driven in-process.
flowchart TB
Entry["om-mcp.ts<br/>vault resolution · stdio · qmd child lifecycle"]
Proto["mcp-protocol.ts<br/>JSON-RPC framing · identity handshake"]
Ctx["mcp-context.ts<br/>vault root · manifest · index name<br/>INSTRUCTIONS · PROMPTS"]
Server["mcp-server.ts<br/>method → behaviour wiring"]
Tools["mcp-tools.ts<br/>tool declarations + annotations"]
Exp["mcp-exposure.ts<br/>which notes are served"]
Qmd["mcp-qmd-client.ts<br/>qmd MCP client + result filter"]
Graph["mcp-graph.ts<br/>expand: links out, links back"]
Caller["mcp-caller.ts<br/>identity · sanitise · audit log"]
Bridge["mcp-memory-bridge.ts<br/>memory ↔ vault seam"]
Capture["mcp-capture.ts<br/>record_work filing"]
Reason["mcp-reason.ts<br/>tier 3: argv · prompt · spawn · record"]
Promo["memory-promoted.ts<br/>promoted-block resolution"]
RH["read-head.ts<br/>bounded frontmatter prefix"]
subgraph Core["Memory core — no MCP knowledge at all"]
MW["memory-write.ts"]
MR["memory-recall.ts"]
MS["memory-similarity.ts"]
MSup["memory-supersede.ts"]
MD["memory-discover.ts"]
Idx["memory-index.ts<br/>parse cache over the store"]
end
Entry --> Proto
Entry --> Ctx
Entry --> Server
Entry --> Qmd
Entry --> Caller
Server --> Tools
Server --> Exp
Server --> Graph
Server --> Bridge
Server --> Capture
Server --> Reason
Server --> Promo
Promo --> Exp
Server --> Idx
Server --> Core
Server --> Qmd
Idx --> MR
Graph --> Exp
Qmd --> Exp
Bridge --> Core
Bridge --> Qmd
Exp --> RH
Bridge --> RH
MD --> RHThe memory core knows nothing about MCP. That is what lets the epistemic contract be hammered by tests without an MCP client, a vault on disk, or a search index.
memory-promoted.ts carries the memory- prefix and sits deliberately outside that subgraph: resolving a promoted block means asking the exposure policy, so it imports mcp-exposure.ts and would break the invariant the box states. The prefix names the domain it serves, not the layer it lives in.
Connection: the identity handshake
Almost everything this server does depends on who is asking. MCP provides that: the server can ask the client which directories the calling session has open (roots/list). It is derived from the client, never declared by the caller, so there is no argument through which a session could claim to be a project it is not.
The complication is that the handshake is asynchronous, and the client's first resources/list arrives before it resolves.
sequenceDiagram
participant Client as Calling session
participant Server as om server
participant Repo as The repo on disk
Client->>Server: initialize
Server-->>Client: capabilities + INSTRUCTIONS
Note over Client: instructions land in the system prompt
Client->>Server: notifications/initialized
Server->>Client: roots/list (server-initiated)
par the client asks early
Client->>Server: resources/list
Note over Server: WAITS on identityReady, capped at 2s
and identity is still in flight
Client-->>Server: roots result
Server->>Repo: read .om-project, else use folder name
Note over Server: identity resolved, gate opens
end
Server-->>Client: resources scoped to this callerAnnouncing notifications/resources/list_changed after the fact was tried first, and the client did not re-fetch — the listing simply stayed unscoped. The deterministic fix is the wait: something that scopes by caller must not answer before it knows the caller. The cap matters too — a client that never answers gets an anonymous, general-only view rather than a hang.
The same gate reopens on notifications/roots/list_changed. Without that, a tool call arriving between the notification and the reply was served under the stale identity: the same race, one step later in the lifecycle.
Identity resolution:
flowchart TB
R["the first root URI<br/>from the handshake"] --> P["rootToPath<br/>file:// → plain path"]
P --> M{".om-project<br/>at the root?"}
M -->|"yes, and it is a valid name"| D["declared identity"]
M -->|no| F["folder name, lowercased"]
D --> Out["caller project"]
F --> Out
Out --> Plat["platforms: read from<br/>the project's own vault note"]The folder name is right until it isn't: two repos both called api share one identity and therefore each other's memories. .om-project resolves that, and health reports which source was used so the collision is discoverable rather than mysterious.
Which notes the server serves
flowchart TB
Start["resolveExposure"] --> A{"mcp_exposed_roots<br/>declared?"}
A -->|yes| Man["source: manifest"]
A -->|no| B{"user_content_roots<br/>present on disk?"}
B -->|yes| Der["source: derived"]
B -->|no| Fall["source: fallback<br/>brain, reference"]
Man --> Strip["strip the memory root — always"]
Der --> Strip
Fall --> Strip
Strip --> Walk["walk each root, max depth 4"]
Walk --> S{"entry is a symlink?"}
S -->|no| C{"filename in<br/>mcp_never_expose?"}
S -->|yes| Cont{"target resolves<br/>inside the root?"}
Cont -->|"no, or broken"| Drop["not served"]
Cont -->|yes| C
C -->|yes| Drop
C -->|no| E{"tagged private<br/>in frontmatter?"}
E -->|"yes, or unreadable"| Drop
E -->|no| Serve["served"]The default is the vault's own user_content_roots, at the granularity the manifest declares them — work/active/, not all of work/. Roots are path prefixes, matched on whole segments, so work/active does not admit work/active-secrets.
mcp_exposed_roots narrows that, and exists for the unusual vault holding material that is not the user's to share — employer-confidential notes, a client's data. Both exposure keys ship empty: the template must not impose one vault's sensitivities on every install.
What this list is, and is not. It decides which notes the server serves. It is not an egress control: a session started with
--add-dirreads the whole vault regardless, so narrowing the read surface prevents nothing on its own. Keeping vault material out of a public PR is the job of the prohibition ininstructions— the form measured to hold — plus the audit log. Reading this list as a security boundary leads to a narrow default, and a narrow default fences off the user's own project notes, which are the single most useful thing a coding session could read.
Five things hold regardless of configuration:
memories/is never served as an ordinary note. Memories carry their own declared reach, evaluated per caller; the note surface would bypass it. It is stripped from the root list unconditionally, and again during the walk.A symlink is contained before it is followed. The walk
lstats each entry — which describes the entry rather than its target — and resolves any link against the root's realpath. Enumerating withstatinstead means a.mdlink inside an exposed root pulls in a file from anywhere on disk.mcp_never_exposewithholds every case spelling, not the one that was declared. The admit side has always compared case-insensitively because the filesystem does; the withhold side was exact-case, and on Windows and macOS that asymmetry served the file —["SOUL.md"]refusedbrain/SOUL.mdand returnedbrain/soul.md, so the listing withheld a note the reader handed over.realpathSyncon Windows does not canonicalise case, so the post-realpath re-check saw the caller's spelling too. The comparison owns the rule rather than the constructor, because a policy assembled any other way would otherwise fail open — and every test assembles one by hand.Frontmatter that does not close inside the head window is private.
readHeadstops at 1200 characters, soprivate: truesitting past that read as public on every surface. A file whose frontmatter cannot be fully inspected is withheld, which is the rule already applied to a file that cannot be read at all.Every read is logged to
.claude/om-mcp-audit.jsonl(gitignored, rotated at 5 MB, one generation kept) with the calling repo — so "what did that session actually see" is answerable afterwards. Frontmatter-derived fields in that line are length-bounded and the promoted list is capped: the log rotates by size, so an unbounded field lets a hostile note erase the very history this design leans on.
Two questions, one answer each — and they are different questions. Which notes exist resolves through visibleFiles: search filters its hits against it, expand computes backlinks only over it, and the resource enumerators build from it. May this one path be read resolves through resolveExposedNote: the resource resolver calls it, and so does the promoted-block path below. Each is one implementation rather than several, because the recurring defect in this layer is a second read path that reaches notes by its own route and applies a different rule.
The symlink case is that defect in its most recent form, and worth keeping as the worked example: resolveResourceUri did realpath containment from the start, while the enumerator followed links silently. So the resource listing published an out-of-vault file's description and expand returned its body, while reading the very same URI was refused. Both ends now contain against the matched declared root — not the first path segment, since roots are prefixes and work/active/ and work/1-1/ share one.
That defect recurred once more, which is why the second predicate is now named rather than left implicit inside the URI resolver. recall gained the ability to serve a promoted block out of brain/ (below), and shipped with its own root check: it dropped neverExpose, dropped isPrivate, and compared the first path segment against roots that are prefixes. So it served two classes of note every other surface withholds, while refusing most of the vault's own declared roots. Every test passed — the fixture policy was ["brain", "projects"], a shape no real policy has. resolveExposedNote was extracted out of resolveExactResourceUri so both callers ask it rather than re-deriving it. A new surface calls one of the two predicates above; it does not grow a third.
Serving a promoted block through recall
recall read the memory root and search/expand read everything but, so the two were disjoint. A lesson promoted from a capture into a brain/ topic note therefore exists twice, and a foreign repo can only reach the capture — the version as first written, which may predate a correction swept through the promoted one.
Since promotion is additive (the capture stays), the capture is still the reach record and is already correct. So visibility is computed exactly as before, from the capture's scope/projects/platforms, and only the content changes: when the capture's promoted: marker carries an anchor, recall serves the promoted text instead of the capture body.
| condition | behaviour |
|---|---|
promoted: brain/Note (no anchor) | named, never served — the pre-existing behaviour |
promoted: brain/Note#^om-id | the block is served, and the facet line says so |
promoted: brain/Note#Heading | the section is served, up to the next heading of the same or higher level |
promoted: brain/Note#TheH1 | refused — a level-1 heading is the note's own title, so it addresses the whole note |
| anchor no longer resolves | capture body, marked STALE — never the whole note |
| the policy withholds the note | capture body, marked withheld |
Four things bound it. Serving is opt-in: only an anchored marker serves, and anchors are written at promotion time, so pointing at a block is a deliberate act rather than an automatic consequence of the marker existing. The policy still decides, via resolveExposedNote — a private-tagged or never-exposed note is refused here exactly as everywhere else. It degrades rather than widens: a stale anchor returns the reason, because serving a whole Gotchas note because one bullet in it was promoted is worse than serving nothing. And what it serves is capped at 40 lines, with the caller told when the cap bit — a level-1 heading is refused outright, since in this vault's shape the H1 is the note's own title and #Some Note would address the entire note through an anchor that looks specific.
Two things that look like content are not: a block id inside a fenced code block is skipped, because /om-tidy teaches the id syntax inside fences and a brain/ note about this feature therefore carries decoys; and a marker containing a newline is refused, because the facet line is built from frontmatter and is not heading-demoted the way a memory body is, so \n\n## FORGED ENTRY added an entry to the response that a reading agent could not tell from a real one.
The audit line carries the promoted notes and their statuses. This is the only surface that reads outside its own root, and the audit log is the stated mitigation for the exposure list not being a security boundary — a recall that served a brain/ block while logging only a count would leave "what did that session actually see" unanswerable.
The residual risk, recorded rather than solved: a promoted block can say more than the capture it came from. The capture's scope was declared for its own text, and once merged into a topic note the block sits beside other material. Opt-in-by-anchor is the mitigation, not a proof.
The incentive gradient, and who hears about decay
Opt-in has a cost that took a real vault to notice: the cheapest way to clear the memory-inbox flag is the form that serves nothing. Any non-empty promoted: drains the count, so a bare marker buys the same hygiene outcome as an anchored one for strictly less work. In a real store, 35 of 37 captures carried a marker and none carried an anchor — the serving path above was dead in production while every flag read as clean.
The fix is not to require an anchor. A bare marker is a legitimate promotion — it records that the lesson was copied — and forcing an anchor to clear hygiene would push anchors onto captures whose promoted block is not fit to leave the vault, which is the exact disclosure risk the paragraph above is careful about. So the gradient is made visible rather than mandatory:
healthreportsPromotions: N servable, M named only, unconditionally. Without it a store where nothing is servable is indistinguishable from one where everything is, because the flag falls either way.healthwarns on genuinely broken promotions — a stale anchor, a withheld or missing target — and names the capture to edit rather than thebrain/note, because the capture is the file that carries the marker.The hygiene line repeats the split only when the inbox flag is already firing. It never raises a flag of its own:
namedOnlycannot revive the count, or the inbox stops being able to reach zero — the permanently-unclearable failure that additive promotion was designed around.
The diagnostic itself already existed; only recall consumed it, which put it in front of a foreign repo — the one party that cannot see brain/ and therefore cannot fix anything. health is the surface a vault session reads, so it is where the report belongs.
A search call, end to end
sequenceDiagram
participant Session as Calling session
participant OM as om server
participant Exp as mcp-exposure
participant QC as qmd client
participant QMD as qmd MCP server
Session->>OM: tools/call search with query and limit
OM->>OM: await identityReady()
OM->>Exp: allowedSearchPaths(vault, policy)
Exp-->>OM: set of vault-relative keys
OM->>QC: qmdSearch(allowed, query, limit)
QC->>QC: subQueries → lex + vec
QC->>QMD: tools/call query, limit = max(limit*4, 20)
QMD-->>QC: structuredContent.results — the WHOLE vault
QC->>QC: filter each hit against allowed
QC-->>OM: text + withheld + total
OM->>OM: audit the call — query, withheld, total
OM-->>Session: ranked passages with note pathsFour details in that flow are decisions rather than mechanics:
The server is an MCP client of the vault's own qmd server. Reusing the existing launcher inherits two fixes for free — the Windows .cmd shim workaround and the named-index pin — and the launcher is located rather than hardcoded, because a stale path here kills search silently.
Filter the result, never the query. The index covers the whole vault, including memories and any folder outside the served roots. Filtering the query would require every caller to construct a scoped query correctly; filtering the result means no query a caller can write returns more than the policy serves. Refusing to answer when structuredContent is absent belongs to the same rule — qmd's human-readable summary carries note paths too, so falling back to it would return results nothing ever checked.
Over-fetch, then trim. Because the filter runs on the result, asking qmd for exactly limit means a vault with much unserved content returns far fewer than requested with no sign that more existed. The client asks for max(limit * 4, 20).
lex and vec, and deliberately nothing else. Keywords find the exact term, vectors find the note that answers the question without using the word. There is no hyde sub-query, because qmd treats hyde as a label on a vector search rather than as a mode — as of 2.8.3 every site in its store reads type === 'vec' || type === 'hyde', and generation happens only inside expandQuery(), which the plain-text query parameter reaches and a searches array bypasses. Passing the raw question in a hyde slot therefore repeats the vec search already going out, and pays for both: measured over 211 human-labelled pairs, sending it never, conditionally or always gave identical rank-1 and found@5 to four decimal places for +35-40ms a query. An authored hyde — a caller-written passage describing what the answer would look like — is a real technique and the SubQuery type still allows it; handing it the user's question is not that.
Degradation is explicit. qmd is optional in this template. A failed call degrades that one search and says so; it must never present as "the vault is empty". The client tracks liveness, so one qmd crash does not disable search for the life of the server, and the entry script replaces a dead child behind a 5-second cooldown so a permanently-broken qmd cannot fork a process per call.
Which memories reach which repo
This is the part the layer exists for. Every memory declares its reach when written, and a reader never widens what a writer declared.
scope: project # general | platform | project
projects: [atlas, atlas-api] # a LIST — the multi-valued axis
platforms: [ios]
confidence: verified
origin: atlas # derived from the roots handshake, not caller-asserted
session: 2026-07-26T14:02:11ZThe list is the load-bearing choice. A memory is multi-valued; a folder is not. A lesson touching two projects and a cross-cutting theme has no correct folder — you would pick one and lose the rest, or duplicate and have two sources of truth. A memories/<project>/ taxonomy was designed and rejected for exactly this. Time is the only thing in the path, because it is the only single-valued fact about a memory:
memories/2026/07/2026-07-26 <title>.mdVisibility is evaluated in order, and the order is the design:
flowchart TB
M["a memory"] --> S1{"scope: general?"}
S1 -->|yes| V["visible"]
S1 -->|no| S2{"the projects list names<br/>the calling repo?"}
S2 -->|yes| V
S2 -->|no| S3{"scope: platform<br/>AND platforms overlap?"}
S3 -->|yes| V
S3 -->|no| H["not visible — default deny"]generalreaches everyone. The only scope that does, which is why the write path polices it hardest.An explicit project listing always wins, whatever the declared scope. This is what makes the multi-project case work:
projects: [a, b]reaches both, and neither has to know the other exists.platformreaches any caller sharing a platform — an iOS lesson reaches the next iOS app and must not reach the web one.Otherwise, not visible. A near-miss does not surface.
A caller with no roots sees general only: the safest reading of "I don't know who you are", and it degrades to useless rather than to wide-open.
Ranking, once visibility has decided the set:
flowchart LR
A["superseded sinks<br/>below live"] --> B["specificity<br/>project > platform > general"]
B --> C["date, newest first"]
C --> D["session timestamp<br/>breaks same-day ties"]A memory naming only your project outranks one naming five. date is day-granular, so without the session tiebreak a just-written memory could sort arbitrarily and fall outside the caller's limit.
Adding a query on top:
sequenceDiagram
participant S as Session
participant OM as om server
participant MR as memory-recall
participant QC as qmd
S->>OM: recall {query, limit, explain}
OM->>MR: recall(vault, caller)
MR-->>OM: visible set, ranked by declaration
alt query given and more than one result
OM->>QC: semanticMemoryOrder(query, visible)
alt index answered
QC-->>OM: reordered
else index down or no structured results
QC-->>OM: null
OM->>OM: lexical fallback — per-token, title AND body
end
OM->>OM: regroup — live first, superseded after
end
OM-->>S: entries with facets, reasons, and withheld countThree properties in that path each closed a real defect:
Retrieve semantically, then filter by visibility — never the reverse. The index sees every memory including other projects'. Applying the scope rule to the result keeps one implementation of it, so semantic recall returns exactly the set plain recall would.
Semantic ordering REORDERS; it does not filter. An earlier version returned only the memories the index matched, so a just-written memory — no embedding yet — vanished from recall at every limit. Anything the index did not place is appended in declared order.
Relevance orders within groups, never across the supersession boundary. Reordering by relevance alone puts a corrected-away fact above the correction that replaced it, which is the one thing supersession exists to prevent.
A missing index degrades ordering, never availability. "The vault knows nothing" is not an acceptable answer to a wiring problem.
Reading the store without re-reading it
recall and the duplicate scan read the same files and parse the same frontmatter, once per call each. That is linear in the store, and it runs on every recall and every write. memory-index.ts caches the parse — and only the parse.
flowchart TB
Call["recall, or the duplicate scan"] --> List["list the store<br/>every call"]
List --> Stat["stat each file<br/>every call"]
Stat --> Q{"size AND mtime<br/>unchanged?"}
Q -->|yes| Hit["reuse the parsed entry"]
Q -->|no| Read["read + parse, then cache it"]
Hit --> Out["entries"]
Read --> Out
Gone["a file that vanished"] --> Drop["dropped from the cache,<br/>so it tracks the store"]The listing and the stat happen on every call. Only re-reading and re-parsing an unchanged file is skipped, which is what makes this safe on the duplicate path: a memory written a second ago is always seen, and a stale view there would admit a duplicate permanently, because nothing downstream re-checks.
Size and mtime, because either alone is weak — some filesystems keep mtime to a whole second, and supersession rewrites frontmatter in place, which can preserve length. Writers also invalidate explicitly, so correctness never rests on timestamp resolution.
It is deliberately in-process, not on disk. Measured, these walks are a minority of a queried recall — the local query embedding dominates — so persisting the index buys a fraction of one operation while adding a file that can rot, disagree with the store, or need migrating in every vault that ever installed the template. The server is long-lived per repo, so every call after the first is warm and a cold start costs what it always did.
The same reasoning trimmed a second cost. Several call sites inspected only a note's frontmatter — is it private, what is its description, what are its aliases, was it agent-written, which platforms does the calling repo declare — and each read the whole file to look at its first kilobyte. visibleFiles does that for every note it enumerates, on every search, expand and write, so the cost scaled with the vault's total bytes: one long reference note made every unrelated call slower. read-head.ts reads a bounded prefix instead, preserving the exact string the old expression produced.
The write path
flowchart TB
Call["remember — title, body,<br/>confidence, scope, projects"] --> Vault{"is the caller<br/>the vault itself?"}
Vault -->|yes| Refuse["refused — a memory written here<br/>reaches only sessions that<br/>already read every note"]
Vault -->|no| Neut["neutralise dangling wikilinks<br/>in title AND body"]
Neut --> Val["validateMemory<br/>epistemic contract"]
Val -->|fails| Report["refused, with reasons"]
Val --> Sup0["resolve supersedes, in the caller's reach —<br/>the named targets drop out of the comparison"]
Sup0 --> Dup{"near-duplicate against the rest?<br/>facet-gated, wherever the new<br/>memory would be read"}
Dup -->|"yes, and force is not set"| Collide["reports what it collided with, if the caller<br/>can see it — suggests supersedes, or names the<br/>supersedes titles that matched nothing;<br/>out of reach, refuses without naming it"]
Dup -->|no| Links["resolve links — emitted only if the target exists"]
Links --> Write["claimFile: atomic exclusive create"]
Write --> Sup["mark superseded memories,<br/>kept and back-linked"]
Sup --> Idx["reindexSync"]
Idx --> Done["Recorded: path, scope, warnings"]The vault does not write to its own memory layer. A session inside the vault already reads every note directly, and a memory written there would be scoped to the vault-as-a-project — reaching only sessions that by definition did not need it. Write-only by construction, so it is refused rather than allowed to accumulate.
The claim is atomic. writeMemory originally used check-then-write. Six processes reporting success produced four files — silent loss, reproducible only across real processes, and one-server-per-repo is the actual deployment shape. It is now an exclusive create (COPYFILE_EXCL), guarded by a test that spawns real processes, since a single-threaded test serialises the calls and always passes.
Re-indexing splits by what each step guarantees:
flowchart LR
W["a memory is written"] --> U["qmd update<br/>SYNCHRONOUS, bounded at 20s"]
U -->|"failed"| Warn["the response says so —<br/>reporting success would be<br/>a lie the caller cannot detect"]
U -->|"ok"| E["qmd embed<br/>DETACHED, unawaited"]
E --> R["ranking improves<br/>moments later"]update decides whether the note is retrievable at all, so it is synchronous. embed only decides where it ranks, and recall appends everything the index did not match in declared order — so waiting on a local model run for ordering that corrects itself moments later buys nothing. Measured, it was most of the write.
The vault normally re-indexes from a PostToolUse hook, but an MCP write is not a Claude Code tool call, so no hook fires. Without this step a note sits on disk and cannot be found, which is worse than no note because it looks like the system worked.
record_work is the sibling tool, and the two are constantly confused. The distinction is single-valued vs multi-valued: a work record is about one project at one moment, so it has a correct folder and gets filed into it. Routing is delegated to the calling session — which already carries the vault's conventions and can search to see where similar notes live — and the server's job is to validate, never to guess. A caller-supplied folder must resolve inside a declared root, with containment checked against that root rather than merely the vault: brain/../work passes a first-segment check and still resolves inside the vault, which is how a capture once landed in a folder nobody named.
Tier 3: reasoning, and why it has to spawn
Everything above answers without inference. search returns passages, expand returns neighbours, recall returns memories in a defensible order. None of them can answer "is what I am about to do consistent with what these six notes decided, and what do I do about the two that disagree?" — retrieval hands you the notes. Something has to read them.
The server cannot borrow the calling session's inference to do that. MCP has a name for it — sampling, where a server asks its client to run a completion — and Claude Code does not implement it (anthropics/claude-code#1785). So reason starts a session through the user's own CLI.
flowchart TB
Q["reason(question)"] --> Seed["tier-1 search, 6 hits<br/>inside the exposure policy"]
Seed --> HasEv{"any hits the caller<br/>may actually see?"}
HasEv -->|yes| P1["prompt: start from these passages,<br/>read further when they are thin"]
HasEv -->|no| P2["prompt: the INDEX was silent,<br/>the vault may not be —<br/>go read it yourself"]
P1 --> Spawn
P2 --> Spawn
Spawn["spawn claude, async, cwd = vault root"] --> Flags["--strict-mcp-config + empty server map<br/>--tools Read,Grep,Glob<br/>--output-format json<br/>stdin closed"]
Flags --> Log["audit — EVERY run, before any refusal:<br/>question, cost, turns, terminal,<br/>model asked, model used, wall ms, roots"]
Log --> Run{"answer came back?"}
Run -->|no| Ref["refuse by name — timeout, early end,<br/>or could-not-start — with the evidence"]
Run -->|yes| Rec["write the record<br/>.claude/om-reasoning/, gitignored"]It runs on the caller's own model, by running on none of its own. MCP gives a server no way to see which model the calling session is using, so there is nothing to mirror. Passing no --model at all makes the spawn take the user's CLI default — whatever they get typing claude — which is the closest reachable thing to the level they are already working at. A vault that wants something else sets reason.model. Bare aliases — haiku, sonnet, opus — are dropped in favour of inheriting, because --model haiku is not honoured and does not error: it silently runs claude-sonnet-5, and a pin that quietly means a different model is worse than no pin. Anything else is passed through as written rather than matched against a shape, since an id-shaped allow-list drops real ids it did not anticipate. Every answer names the model that actually ran, and says whether that was the pinned one.
Usage is answered by the record, not by a limit. Every invocation is appended to the audit log — before any refusal, so a run that produced no answer is still recorded — with the question, cost, turns, terminal reason, model asked for, model that ran, wall time, and the roots the spawn was given. health reports the day's figure from the tail of that log, and says at least when the log was too large to read whole: a number standing in for a cap has to admit being a floor rather than quietly under-report on the busiest day. The spawn is Claude, on the user's machine, under the user's auth, on the account the calling session already runs on; a server-side bound there would be this layer rationing the user's own resource back to them. The one bound present is a 300-second timeout, which kills a hung child — a failure, not a preference.
Isolation is what stops it recursing. --strict-mcp-config pointed at a config declaring no servers leaves the spawn with no MCP at all, so it cannot call back into om and start a tree that multiplies at every level. Verified on the wire: a spawn under those flags reports no MCP servers available.
Read-only comes from restricting the toolset, not from a permission mode. --tools Read,Grep,Glob removes the writing tools outright, so an attempted write returns No such tool available: Write rather than waiting on a prompt nothing is there to answer. --permission-mode plan was the first attempt and is wrong twice over: it writes the plan under ~/.claude/plans/, outside the vault, and it changes what the spawn believes it is producing — in a measured run the model composed its analysis as a plan, could not hand it back without ExitPlanMode headlessly, wrote the content to a file and returned a one-line note about having done so. The whole answer was lost, and the failure is non-deterministic, so it survived the first round of testing.
A related trap, since it is how that one nearly got mis-diagnosed: a spawn's account of its own tools is unreliable. Asked to list them under this allowlist, it returns the full default set including Write. Asked to actually write a file, it cannot. It reports its prior, not its runtime — so capability is checked by trying, never by asking.
The spawn is told the same boundary its seed was filtered through. Read,Grep,Glob with cwd at the vault root reaches the whole tree, while the seed came through allowedSearchPaths — so without an explicit boundary the one tool that reads most would be the only one ignoring mcp_exposed_roots, and could quote memories/ scoped to other callers into its answer. The prompt therefore names the exposed roots and prohibits everything else, in the prohibition form measured to propagate. It is a boundary the spawn observes, not one the filesystem enforces, which is exactly why the audit line records the roots it was given: what it was allowed to read stays answerable afterwards.
It runs asynchronously, and that is load-bearing. A synchronous spawn holds the whole event loop for the life of the child — measured at 73–80s, capped at 300s — during which the server parses no stdin at all: no other tool call, no ping, no roots/list reply, and no timers, so even the identity gate's own 2s cap could not fire. The entry script buys concurrency deliberately ("serialising them here would make one slow search block every other call"), and a synchronous spawn took that back harder than serialising would have. Verified on the wire: with reason running for 80.3s, a search and a health issued two seconds in came back at 2.9s and 2.8s.
Two things about the spawn are non-obvious and were found the hard way. stdin must be closed, or the CLI waits on it and the call hangs to the timeout instead of answering. And the binary is spawned directly with argv, never through a shell — on Windows cmd.exe strips the quotes out of an inline JSON argument, and the CLI then tries to open the mangled string as a file path.
The seed prompt has two branches, and the second one exists because of a measured failure.
flowchart LR
E["empty seed"] --> R1["read as: the vault<br/>has nothing on this"]
R1 --> W["reports 'no prior decisions recorded'<br/>about a question answered<br/>one directory away"]
E --> R2["read as: the INDEX<br/>had nothing"]
R2 --> G["go read the vault directly,<br/>and say how you looked"]
style W fill:#5f1e1e,color:#fff
style G fill:#1e4620,color:#fffA vault whose index is missing or stale returns exactly what a vault with nothing to say returns. Under the normal wording the spawn treated the first as the second and reported silence — the same absence-read-as-answer failure this layer already had in retrieval, reproduced one tier up. The empty case therefore gets its own instructions rather than a blank evidence block.
Which branch fires is decided by the permitted hit count, not by qmd's total: total counts what the index returned before the exposure policy filtered it, so a search whose every hit was withheld has total > 0 and nothing the spawn can see. Reading the total alone would hand it an empty evidence block — precisely the shape that produced the false-absence answer. The prose sentinels ((no results), search failed: …) remain as a fallback for callers holding only the text, matched as prefixes so a passage that merely quotes one of those phrases still counts as evidence.
Against a vault whose index is working, seeding is worth about one turn — less than it looks like it should be, because a spawn with cwd set to the vault already inherits the vault's contract and can search for itself. Its value there is mostly that it starts in the right place, and it carries a bias: in a measured run the seeded answer leaned on the passages and repeated a framing the vault had since corrected. Hence the explicit licence in the prompt to read past the evidence, and the instruction to say when it is thin.
Losing the index entirely costs less than it sounds like it should. Same vault, same question, same build, run solo and sequentially so nothing competes for the machine:
| condition | turns | wall | reported cost |
|---|---|---|---|
| index working, seeded | 23 | 72.8s | $0.37 |
| no index at all, 1,267 notes | 26 | 76.2s | $0.44 |
Three turns and four seconds. The fallback branch does exactly what it is told — enumerates the note tree, greps it, reads every distinct note, correctly identifies the 1,200 synthetic filler notes as filler, reports how it looked, and only then says nothing is recorded — and a spawn sitting in the vault turns out to be good at that. The index makes tier 3 cheaper, not viable. The thing that would make it useless is the failure above: concluding silence without looking.
Both figures are from the shipped flags. An earlier measurement here showed 143s for the unindexed case and was wrong about the cause — it was taken under
--permission-mode plan, where the turns went into composing a plan rather than into reading the vault.
An early end returns no answer, not a short one. If the spawn errors, times out, or stops before producing a result, the tool refuses and hands back the evidence the search already found. A truncated synthesis presented as a complete one is the single outcome worse than admitting there isn't one.
Answers are not memories. The record lands in .claude/om-reasoning/ — gitignored, outside the note tree, skipped by every read surface because it is a dot-directory — carrying confidence: inferred. Auto-recording it would fill the store with machine conclusions nobody asked for and nobody verified, and the epistemic contract exists precisely to stop that. The calling session decides whether any of it earns a remember.
Failure modes, and how each is made visible
Every failure in this layer presents identically as "no results". That is what health exists for.
| failure | how it presents | what surfaces it |
|---|---|---|
| memory root renamed in Obsidian | recall returns nothing | health reports the discovered root and the drift |
| captures split across two roots | recall sees only one | health warns, naming both |
| qmd index belongs to another vault | search returns nothing | health checks index ownership |
| qmd not installed | search degrades to lexical | health reports the launcher missing |
OM_VAULT_PATH points elsewhere | everything describes the wrong vault | health warns when it disagrees with the launcher location |
| caller unidentified | only general memories visible | recall says so in prose; health says ANONYMOUS |
| memory scoped away | absent from recall | recall with explain: true gives counts and reasons |
| two repos sharing a folder name | each sees the other's memories | health reports the identity source and suggests .om-project |
| the CLI is not on the server's PATH | reason cannot start at all | the refusal names the spawn error; OM_CLAUDE_BIN points at the binary |
a pinned reason.model is ignored | answers come from another model, silently | the answer names the model that actually ran, and flags the mismatch |
| a reasoning spawn ends early | would otherwise read as a complete answer | refused by name, with the turn count, the terminal reason and the evidence |
| a reasoning spawn hits the 300s timeout | looks identical to a spawn that never started | the kill signal is read, so the terminal reason is timeout, in the message and the log |
| a promoted anchor goes stale | recall quietly serves the raw capture instead of the corrected text | health names the CAPTURE to edit, and why |
| every promotion is decorative | the inbox flag clears and nothing is servable | health reports servable vs named-only; hygiene repeats it once the flag is already firing |
| the server exits mid-spawn | a session runs on with no bound and no audit line | shutdown kills every in-flight spawn |
| a promoted anchor no longer resolves | the capture body is served, silently older than the vault's own text | the facet line marks the anchor STALE; the audit line records stale-anchor |
a promoted note is outside the exposed roots, private, or withheld by name | the capture body is served rather than the correction | the facet line says it is withheld; the audit line records not-exposed |
| a promoted note is missing or unreadable | the capture body is served | the facet line says unreadable; the audit line records unreadable |
A stray VAULT_PATH was honoured at one point, and that name is too generic to claim — it is set for unrelated reasons on real machines, and the result was a server serving a different vault while reporting that vault's config as if correct. Only OM_VAULT_PATH is read now.
The install is two steps, and both are required
{
"mcpServers": {
"om": {
"command": "node",
"args": ["<absolute path to your vault>/.claude/scripts/om-mcp.mjs"]
}
}
}That goes in the consuming project's .mcp.json. Then add a short section to that project's own instruction file (CLAUDE.md or AGENTS.md) telling it the vault exists and to consult it.
Step 2 is not documentation garnish. Measured: with the server wired and no repo-side instruction, a session made zero vault calls and implemented a design the vault had recorded as explicitly rejected. With the instruction present, it refused and cited the note. This follows directly from the prohibition/routing asymmetry above — and it makes the repo-side snippet a shipped deliverable, since a server installed without it is a server that gets ignored.
Do not register the raw
qmdserver in a consuming repo. It searches every note directly, with no notion of which memories were written for which project, so the repo matches against lessons meant for unrelated ones. Applying declared scope on top of the index is exactly whatomadds, and going around it returns the wrong things, not merely more of them.
Multi-Agent Portability
The same scripts serve three agents. Each agent has its own config file mapping equivalent lifecycle events to the shared scripts. The checklist runs on Stop for Claude Code and Codex (per response, deduped) and on SessionEnd for Gemini (at shutdown). The script branches on the documented event name and on what a payload field means, never on which agent sent it. Where agents spell one field differently, every spelling is read, so the field can never tell the agents apart: Grok's Stop sends the re-entry flag as stopHookActive, and the checklist reads it beside stop_hook_active (#323). Matchers differ the same way. Claude Code and Codex read a SessionStart matcher as a regex over the source (startup|resume|clear|compact). Gemini compares a lifecycle matcher to the source as an exact string, with "" or "*" matching all (gemini-cli hookPlanner.ts, as of 2026-10-13). A startup|resume matcher once matched nothing there, so session-start never ran under Gemini (#352). tests/hook-config.test.ts checks every agent's SessionStart matcher under that agent's own rule.
Session-boundary output is JSON-or-nothing on all three agents. On the hook path session-start.ts prints one hookSpecificOutput envelope too, for every agent (#340). stop-checklist.ts emits {} when a Stop has nothing new to say. What the user sees is systemMessage, the one user-facing field all three agents share: on a Stop whose findings changed, a one-line-per-section summary, with the full report saved for the next prompt's UserPromptSubmit to hand to the agent unseen. Only when the report cannot be saved does it go out as Stop feedback, which Claude Code prints in full; whether Codex honours Stop feedback is unverified. SessionEnd (Gemini) has no next prompt, so there the full report is systemMessage only. om-wrap-up still owns the deliberate acting pass at the end of a session.
flowchart TB
subgraph Configs["Per-agent config (event name mapping)"]
Claude[".claude/settings.json<br/>Claude Code"]
Codex[".codex/hooks.json<br/>Codex CLI"]
Gemini[".gemini/settings.json<br/>Gemini CLI"]
end
subgraph Shared[".claude/scripts/ (shared core)"]
S1["session-start.ts"]
S2["classify-message.ts"]
S3["validate-write.ts"]
S5["pre-compact.ts"]
S6["stop-checklist.ts"]
S7["qmd-refresh-run.ts<br/>(detached worker, not a hook)"]
end
subgraph Manuals["Instruction files"]
AM["AGENTS.md<br/>(every agent)"]
RM[".claude/rules/*.md<br/>(Claude-only files, and shared<br/>sections AGENTS.md points to)"]
end
Claude --> Shared
Codex --> Shared
Gemini --> Shared
Claude -.reads.-> AM
Claude -.reads.-> RM
AM -.points to.-> RM
Codex -.reads.-> AM
Gemini -.reads.-> AMCommands in .claude/commands/ are plain Markdown prompts. Claude Code invokes them as slash commands. Codex and Gemini treat them as regular prompts (users type om-standup without the leading slash). No SDK binding is required.
Adding another agent means writing one more config file, and making sure it reads AGENTS.md: natively, or through a setting the way Gemini's context.fileName does. There is one instruction file, so there is no second manual to keep in sync. .claude/rules/ holds the Claude-only files and the shared sections AGENTS.md points to: Claude Code and Grok load every file there at session start, and other agents read a shared one when a pointer sends them.
Subagents on other agents
The subagents in .claude/agents/ handle isolated tasks (brag spotting, vault auditing, cross-linking, etc.). The prompt content is agent-agnostic markdown.
Codex CLI: as of the Skills launch (Dec 2025), Codex discovers skills at
.agents/skills/<name>/SKILL.md(directory-per-skill; frontmatter requiresnameanddescription). Mirror each.claude/agents/*.mdinto aSKILL.md, keeping the prompt body intact. See the Codex Skills docs for the full schema.Gemini CLI: agents live in
.gemini/agents/. Copy the files and adapt the YAML frontmatter fields to Gemini's schema.
The om server sits outside this table on purpose. It speaks MCP over stdio and knows nothing about which harness is on the other end, so it needs no per-agent config at all — any MCP-capable client registers it the same way.
Vault-First Memory
There are two memory systems, and the distinction is load-bearing:
flowchart LR
SessionStart["SessionStart hook<br/>(.claude/scripts/session-start.ts)"]
subgraph Ephemeral["~/.claude/ (not git-tracked)"]
MemIndex["MEMORY.md<br/>(auto-loaded index)"]
end
subgraph Durable["Vault (git-tracked)"]
NorthStar["brain/North Star.md"]
BrainIdx["brain/Memories.md<br/>(topic index)"]
Gotchas["brain/Gotchas.md"]
Patterns["brain/Patterns.md"]
Decisions["brain/Key Decisions.md"]
end
SessionStart ==>|reads every session| NorthStar
MemIndex -->|points at| BrainIdx
MemIndex -->|points at| Gotchas
MemIndex -->|points at| Patterns
MemIndex -->|points at| Decisions
BrainIdx --> Gotchas
BrainIdx --> Patterns
BrainIdx --> DecisionsTwo load paths into a session, both landing in the vault:
Pointer indirection —
~/.claude/.../MEMORY.mdis Claude Code's private auto-memory directory. The template uses it only to hold a thin index that points at vault locations. Topics fire on demand when the conversation touches them.Direct injection —
brain/North Star.mdis loaded by theSessionStarthook on every session as its own context block. It's the goals document; it needs to be present every time, not only when triggered.
All actual memory content lives in brain/ as real Obsidian notes — queryable by QMD, visible in the graph, and shared across every agent (Claude Code, Codex, Gemini) because they all read the same vault.
The rule that enforces this: "when asked to remember, write to the relevant brain/ topic note, not to ~/.claude/." It is restated in AGENTS.md because it is the easiest rule to break and the hardest to detect breaking.
Two stores, one vault
brain/ and memories/ look similar and answer different questions. Keeping them apart is deliberate.
brain/ | memories/ | |
|---|---|---|
| written by | a session inside the vault, or you | a session in another repo, through om |
| shape | curated topic notes, edited over time | append-only atomic entries under YYYY/MM/ |
| reach | the whole vault, always | declared per entry — scope, projects[], platforms[] |
| corrections | edit the note | a new entry that supersedes the old, which is kept and back-linked |
| browsing | the note itself, and the graph | bases/Memories.base — Recent, By project, Needs review, Superseded, General reach |
| served as an MCP resource | yes, if inside an exposed root | never — reach is per caller, so the note surface would bypass it |
Both are Markdown in the vault, both are in the graph, both are git-tracked. The difference is that a brain/ note is a document you maintain, while a memory is an immutable claim with a declared audience and a confidence level.
The third store is ~/.claude/.../MEMORY.md, which holds no content at all — only pointers. The template creates nothing else there, and validate-write.ts blocks attempts to.
Skills and Commands
The template ships two categories of skills:
Obsidian-native skills (
kepano/obsidian-skills) in.claude/skills/— teach the agent Obsidian-flavored Markdown, the Obsidian CLI, Bases, and JSON Canvas. Loaded automatically when relevant.A custom QMD skill in
.claude/skills/qmd/— teaches the agent the preference order for vault retrieval (MCP tools when registered →qmdCLI as fallback → Grep/Glob as last resort) and the signals that should trigger a proactive search (past decisions, incidents, people, architecture, duplicates before creating a note).
Slash commands in .claude/commands/ are operational workflows (e.g. /om-standup, /om-wrap-up, /om-review-brief). Each is a Markdown file with prompt instructions. Subagents in .claude/agents/ are invoked by those commands to keep heavy operations (Slack archaeology, PR deep scans, vault migration) out of the main context window.
The rule for adding a new command: if it produces durable knowledge, it should write to the vault. If it would only make sense within one session, it is probably better as a prompt pattern than a command.
Extension Seams
The design makes these changes easy:
| Change | Touch this |
|---|---|
| Add a new note type | vault-manifest.json → frontmatter_required, and a template in templates/ |
| Isolate this vault from others on the same machine | automatic (folder-derived); override with vault-manifest.json → qmd_index, then re-bootstrap |
| Add a new classification category | .claude/scripts/classify-message.ts + AGENTS.md routing rules |
| Add a new lifecycle behavior | A new script in .claude/scripts/ wired into all three agent configs |
| Add a new agent (Cursor, Windsurf, …) | New config file mapping events to existing scripts; the agent reads AGENTS.md |
| Add a new subagent | A new Markdown file in .claude/agents/, referenced from the command that invokes it |
| Add a new Base view | A new .base file in bases/, embedded from Home.md if it should surface |
Change which notes om serves | vault-manifest.json → mcp_exposed_roots / mcp_never_expose, or tag a note private |
| Move the memory store | Rename the folder in Obsidian — discovery finds it and health reports the drift; pin it with memory_root to be explicit |
Add a new om tool | A declaration in .claude/scripts/lib/mcp-tools.ts + a case in mcp-server.ts — the description is what the model reads when deciding to call it |
| Change what the eager layer may spend | vault-manifest.json → eager_layer_budget_bytes (hook path), eager_layer_instruction_budget_bytes (mod path), listing_collapse_threshold |
The design is hostile to these changes (on purpose):
Storing memories outside the vault. The whole point of
brain/andmemories/is portability and graph visibility.Bypassing the manifest. If a new component needs to know the index name, the memory root, or the infrastructure boundary, it should read the manifest rather than hardcode.
Hardcoding agent event names inside scripts. Event name translation is a config-layer concern.
Scoping vault notes per calling repo. An earlier revision did this — a repo saw only its own notes — and it killed the most valuable measured capability, because the answers worth having come from connecting projects. Memories declare their reach; notes do not.
Adding a second read path. Any new surface that answers "which notes exist" must resolve through
visibleFilesrather than walking the vault itself — and any surface that answers "may this one path be read" must callresolveExposedNoterather than re-checking roots. Both have already been re-derived once each, and both re-derivations shipped serving notes the policy withholds.
Upgrade Path
Template versions are tracked in vault-manifest.json with fingerprints that let /om-vault-upgrade detect an older vault's version by presence or absence of specific files. The migrator uses the infrastructure/user-content split to decide what to overwrite versus preserve. CHANGELOG.md documents what changed in each version.
The long-term stability guarantee is narrow: the manifest keys (qmd_index, infrastructure, user_content_roots, frontmatter_required, memory_root), the hook script names under .claude/scripts/, the om-mcp.mjs launcher path that consuming repos put in their .mcp.json, and the folder layout for user content. Everything else — including command names, subagent internals, and classification logic — is allowed to evolve between versions.
memories/ is listed in user_content_roots, so an upgrade preserves it rather than treating it as template infrastructure. The launcher path is on this list because it is the one string that lives outside the vault, written into other repositories' config — moving it silently breaks every consumer.
Install Paths
Two ways to bring obsidian-mind into a new directory:
git clone — the original. Clone the repo, open the folder in Obsidian, talk to the agent. Zero machinery beyond git. Every file ships verbatim; no install-time substitution. The hook scripts under .claude/scripts/ only run when the agent triggers them. This path is the long-standing default and the one new contributors use to read the codebase.
shardmind install — v6. ShardMind is a package manager for Obsidian vault templates that produces the same vault as git clone plus a .shardmind/ sidecar. A wizard collects four values (user_name, org_name, vault_purpose, qmd_enabled), gates eleven modules (4 always-on content, 4 removable content, 3 agent — claude / codex / gemini), and runs the lifecycle hooks: bootstrap (.shardmind/hooks/bootstrap.ts) initializes git and optionally bootstraps QMD when enabled, and personalize (.shardmind/hooks/personalize.ts) writes the user's name into brain/North Star.md. The shard contract is locked by three invariants:
Invariant 1 — clone-equivalence under defaults.
shardmind install --defaultsproduces a vault byte-equivalent togit clonemodulo Tier 1 exclusions (.git,.github, ephemeral.obsidian/workspace*.json), the engine metadata under.shardmind/, and a vault-rootshard-values.yaml.Invariant 2 — hooks no-op on defaults. Managed-file edits (e.g., the North Star personalization) live in the
personalizehook, which the engine refuses to call when every value is at its default — so the invariant is engine-enforced, not hook-checked. With every value at its default, the install remains byte-equivalent to clone.Invariant 3 — post-update is additive-only. The post-update hook restricts managed-file writes to
ctx.newFiles(paths added by the new version), preventing clobbers of the merge engine's three-way resolution of user edits.
The contract surface lives at .shardmind/shard.yaml (manifest), .shardmind/shard-schema.yaml (values + module declarations), .shardmind/hooks/{bootstrap,personalize,post-update}.ts (lifecycle), and .shardmindignore at repo root (excludes CONTRIBUTING.md, README translations, marketing media from the install). Spec: ShardMind docs/SHARD-LAYOUT.md.
Additive principle. shardmind install produces a strictly larger vault than git clone — never smaller, never different on shared paths under defaults. Deleting .shardmind/ and shard-values.yaml from an installed vault leaves a working clone-equivalent vault. The same is true on the source side: deleting .shardmind/ from this repo would produce a v5.1-shape working vault. ShardMind extends the clone experience; it doesn't replace it.
shardmind update (v6+) three-way-merges your edits with upstream changes — the moat that git pull doesn't provide for templates with installed-time personalization. /om-vault-upgrade remains the path for migrating a v5.x clone or arbitrary vault into v6 in place; once installed, shardmind update (or shardmind adopt for retroactive adoption) takes over.
329 files · 3 not served here
- .claude-plugin/marketplace.json
- .claude-plugin/plugin.json
- .claude/agents/brag-spotter.md
- .claude/agents/context-loader.md
- .claude/agents/correction-sweep.md
- .claude/agents/cross-linker.md
- .claude/agents/people-profiler.md
- .claude/agents/review-fact-checker.md
- .claude/agents/review-prep.md
- .claude/agents/slack-archaeologist.md
- .claude/agents/vault-librarian.md
- .claude/agents/vault-migrator.md
- .claude/commands/om-capture-1on1.md
- .claude/commands/om-correct.md
- .claude/commands/om-dump.md
- .claude/commands/om-humanize.md
- .claude/commands/om-incident-capture.md
- .claude/commands/om-intake.md
- .claude/commands/om-meeting.md
- .claude/commands/om-peer-scan.md
- .claude/commands/om-prep-1on1.md
- .claude/commands/om-project-archive.md
- .claude/commands/om-review-brief.md
- .claude/commands/om-review-peer.md
- .claude/commands/om-self-review.md
- .claude/commands/om-slack-scan.md
- .claude/commands/om-standup.md
- .claude/commands/om-tidy.md
- .claude/commands/om-vault-audit.md
- .claude/commands/om-vault-upgrade.md
- .claude/commands/om-weekly.md
- .claude/commands/om-wrap-up.md
- .claude/memory-template.md
- .claude/rules/brain-topics.md
- .claude/rules/hooks-and-mod.md
- .claude/rules/hooks-per-agent.md
- .claude/rules/memory.md
- .claude/rules/obsidian-cli.md
- .claude/rules/session-workflow.md
- .claude/rules/skills.md
- .claude/rules/subagents.md
- .claude/rules/thinking.md
- .claude/rules/vault-from-another-repo.md
- .claude/scripts/.gitignore
- .claude/scripts/charcount.ts
- .claude/scripts/classify-message.ts
- .claude/scripts/generate-memory-index.ts
- .claude/scripts/lib/active-hygiene.ts
- .claude/scripts/lib/atomic-write.ts
- .claude/scripts/lib/automemory-index.ts
- .claude/scripts/lib/charcount.ts
- .claude/scripts/lib/correction-sweep.ts
- .claude/scripts/lib/env-path.ts
- .claude/scripts/lib/frontmatter.ts
- .claude/scripts/lib/hint-state.ts
- .claude/scripts/lib/hook-io.ts
- .claude/scripts/lib/instruction-files.ts
- .claude/scripts/lib/main-guard.ts
- .claude/scripts/lib/matcher.ts
- .claude/scripts/lib/mcp-caller.ts
- .claude/scripts/lib/mcp-capture.ts
- .claude/scripts/lib/mcp-context.ts
- .claude/scripts/lib/mcp-exposure.ts
- .claude/scripts/lib/mcp-graph.ts
- .claude/scripts/lib/mcp-memory-bridge.ts
- .claude/scripts/lib/mcp-protocol.ts
- .claude/scripts/lib/mcp-qmd-client.ts
- .claude/scripts/lib/mcp-reason.ts
- .claude/scripts/lib/mcp-server.ts
- .claude/scripts/lib/mcp-tools.ts
- .claude/scripts/lib/memory-discover.ts
- .claude/scripts/lib/memory-index.ts
- .claude/scripts/lib/memory-promoted.ts
- .claude/scripts/lib/memory-recall.ts
- .claude/scripts/lib/memory-similarity.ts
- .claude/scripts/lib/memory-supersede.ts
- .claude/scripts/lib/memory-write.ts
- .claude/scripts/lib/om-mod.ts
- .claude/scripts/lib/project-dir.ts
- .claude/scripts/lib/promoted-gate.ts
- .claude/scripts/lib/prose-width.ts
- .claude/scripts/lib/qmd-bootstrap.ts
- .claude/scripts/lib/qmd-cwd.ts
- .claude/scripts/lib/qmd-ignore.ts
- .claude/scripts/lib/qmd-models.ts (not served here)
- .claude/scripts/lib/qmd-refresh.ts
- .claude/scripts/lib/qmd.ts
- .claude/scripts/lib/read-field.ts
- .claude/scripts/lib/read-head.ts
- .claude/scripts/lib/regex.ts
- .claude/scripts/lib/reinject.ts
- .claude/scripts/lib/report-key.ts
- .claude/scripts/lib/session-start-run.ts
- .claude/scripts/lib/session-start.ts
- .claude/scripts/lib/signals.ts
- .claude/scripts/lib/stop-handoff.ts
- .claude/scripts/lib/stop-report.ts
- .claude/scripts/lib/upgrade-plan.ts
- .claude/scripts/lib/wikilinks.ts
- .claude/scripts/lib/write-targets.ts
- .claude/scripts/om-mcp.mjs
- .claude/scripts/om-mcp.ts
- .claude/scripts/package.json
- .claude/scripts/pre-compact.ts
- .claude/scripts/qmd-mcp.d.mts
- .claude/scripts/qmd-mcp.mjs
- .claude/scripts/qmd-refresh-run.ts
- .claude/scripts/session-start.ts
- .claude/scripts/stop-checklist.ts
- .claude/scripts/tests/_helpers.test.ts
- .claude/scripts/tests/_helpers.ts
- .claude/scripts/tests/_proc.ts
- .claude/scripts/tests/_qmd-cache-guard.ts
- .claude/scripts/tests/_race-child.ts
- .claude/scripts/tests/_run-tests.ts
- .claude/scripts/tests/active-hygiene.test.ts
- .claude/scripts/tests/agent-docs-sync.test.ts
- .claude/scripts/tests/atomic-write.test.ts
- .claude/scripts/tests/automemory-index.test.ts
- .claude/scripts/tests/bootstrap-hook.test.ts
- .claude/scripts/tests/budget-ladder.test.ts
- .claude/scripts/tests/capture-markup-guard.test.ts
- .claude/scripts/tests/charcount.test.ts
- .claude/scripts/tests/classify-message.test.ts
- .claude/scripts/tests/correction-sweep-docs-sync.test.ts
- .claude/scripts/tests/correction-sweep.test.ts
- .claude/scripts/tests/delivery-gate.test.ts
- .claude/scripts/tests/frontmatter.test.ts
- .claude/scripts/tests/gemini-commands.test.ts
- .claude/scripts/tests/generate-changelog.test.ts
- .claude/scripts/tests/hint-state.test.ts
- .claude/scripts/tests/hook-config.test.ts
- .claude/scripts/tests/hook-io.test.ts
- .claude/scripts/tests/main-guard.test.ts
- .claude/scripts/tests/manifest-check.test.ts
- .claude/scripts/tests/matcher.test.ts
- .claude/scripts/tests/mcp-caller.test.ts
- .claude/scripts/tests/mcp-capture.test.ts
- .claude/scripts/tests/mcp-context.test.ts
- .claude/scripts/tests/mcp-exposure.test.ts
- .claude/scripts/tests/mcp-graph.test.ts
- .claude/scripts/tests/mcp-memory-bridge.test.ts
- .claude/scripts/tests/mcp-protocol.test.ts
- .claude/scripts/tests/mcp-qmd-client.test.ts
- .claude/scripts/tests/mcp-reason.concurrency.test.ts
- .claude/scripts/tests/mcp-reason.test.ts
- .claude/scripts/tests/mcp-refusal-audit.test.ts
- .claude/scripts/tests/mcp-reindex.test.ts
- .claude/scripts/tests/mcp-required.test.ts
- .claude/scripts/tests/memory-discover.test.ts
- .claude/scripts/tests/memory-index.test.ts
- .claude/scripts/tests/memory-promoted.test.ts
- .claude/scripts/tests/memory-recall.test.ts
- .claude/scripts/tests/memory-similarity.test.ts
- .claude/scripts/tests/memory-supersede-race.test.ts
- .claude/scripts/tests/memory-supersede-same-title.test.ts
- .claude/scripts/tests/memory-supersede.test.ts
- .claude/scripts/tests/memory-write.concurrency.test.ts
- .claude/scripts/tests/memory-write.test.ts
- .claude/scripts/tests/mod-timeouts.test.ts
- .claude/scripts/tests/om-mcp.integration.test.ts
- .claude/scripts/tests/om-mod.test.ts
- .claude/scripts/tests/personalize-hook.test.ts
- .claude/scripts/tests/pre-compact.test.ts
- .claude/scripts/tests/proc.test.ts
- .claude/scripts/tests/project-dir.test.ts
- .claude/scripts/tests/prose-width.test.ts
- .claude/scripts/tests/qmd-bootstrap.test.ts
- .claude/scripts/tests/qmd-cwd.test.ts
- .claude/scripts/tests/qmd-ignore.test.ts
- .claude/scripts/tests/qmd-mcp.test.ts
- .claude/scripts/tests/qmd-models.test.ts
- .claude/scripts/tests/qmd-refresh.integration.test.ts
- .claude/scripts/tests/qmd-refresh.test.ts
- .claude/scripts/tests/qmd.test.ts
- .claude/scripts/tests/read-head.test.ts
- .claude/scripts/tests/record-work-required.test.ts
- .claude/scripts/tests/refresh-in-vault.test.ts
- .claude/scripts/tests/refresh-switch.test.ts
- .claude/scripts/tests/regex.test.ts
- .claude/scripts/tests/reinject.test.ts
- .claude/scripts/tests/report-key.test.ts
- .claude/scripts/tests/sentinel-path.test.ts
- .claude/scripts/tests/session-start.integration.test.ts
- .claude/scripts/tests/session-start.test.ts
- .claude/scripts/tests/shard-contract.test.ts
- .claude/scripts/tests/shardmindignore.test.ts
- .claude/scripts/tests/signals.test.ts
- .claude/scripts/tests/spawn-windows-hide.test.ts
- .claude/scripts/tests/stop-checklist.test.ts
- .claude/scripts/tests/stop-handoff.test.ts
- .claude/scripts/tests/stop-report.test.ts
- .claude/scripts/tests/tidy-fix.test.ts
- .claude/scripts/tests/upgrade-plan.test.ts
- .claude/scripts/tests/validate-write.memory.test.ts
- .claude/scripts/tests/validate-write.test.ts
- .claude/scripts/tests/vault-promoted.test.ts
- .claude/scripts/tests/vault-prose-width.test.ts
- .claude/scripts/tests/vault-wikilinks.test.ts
- .claude/scripts/tests/wikilink-baseline.json
- .claude/scripts/tests/wikilinks.test.ts
- .claude/scripts/tests/write-targets.test.ts
- .claude/scripts/tidy-fix.ts
- .claude/scripts/tsconfig.json
- .claude/scripts/upgrade-plan.ts
- .claude/scripts/validate-write.ts
- .claude/settings.json
- .claude/skills/defuddle/SKILL.md
- .claude/skills/excalidraw-diagram/SKILL.md
- .claude/skills/excalidraw-diagram/references/excalidraw-schema.md
- .claude/skills/json-canvas/SKILL.md
- .claude/skills/json-canvas/references/EXAMPLES.md
- .claude/skills/mermaid-visualizer/SKILL.md
- .claude/skills/mermaid-visualizer/references/syntax-rules.md
- .claude/skills/obsidian-bases/SKILL.md
- .claude/skills/obsidian-bases/references/FUNCTIONS_REFERENCE.md
- .claude/skills/obsidian-cli/SKILL.md
- .claude/skills/obsidian-markdown/SKILL.md
- .claude/skills/obsidian-markdown/references/CALLOUTS.md
- .claude/skills/obsidian-markdown/references/EMBEDS.md
- .claude/skills/obsidian-markdown/references/PROPERTIES.md
- .claude/skills/obsidian-mind/.claude-plugin/plugin.json
- .claude/skills/obsidian-mind/hooks/context.ts
- .claude/skills/obsidian-mind/hooks/hooks.json
- .claude/skills/obsidian-mind/hooks/register.test.ts
- .claude/skills/obsidian-mind/hooks/register.ts
- .claude/skills/obsidian-mind/hooks/stop.test.ts
- .claude/skills/obsidian-mind/hooks/stop.ts
- .claude/skills/obsidian-mind/hooks/world.ts
- .claude/skills/obsidian-mind/types/index.d.ts
- .claude/skills/qmd/SKILL.md
- .claude/update-skills.ts
- .codex/hooks.json
- .gemini/commands/om-capture-1on1.toml
- .gemini/commands/om-correct.toml
- .gemini/commands/om-dump.toml
- .gemini/commands/om-humanize.toml
- .gemini/commands/om-incident-capture.toml
- .gemini/commands/om-intake.toml
- .gemini/commands/om-meeting.toml
- .gemini/commands/om-peer-scan.toml
- .gemini/commands/om-prep-1on1.toml
- .gemini/commands/om-project-archive.toml
- .gemini/commands/om-review-brief.toml
- .gemini/commands/om-review-peer.toml
- .gemini/commands/om-self-review.toml
- .gemini/commands/om-slack-scan.toml
- .gemini/commands/om-standup.toml
- .gemini/commands/om-tidy.toml
- .gemini/commands/om-vault-audit.toml
- .gemini/commands/om-vault-upgrade.toml
- .gemini/commands/om-weekly.toml
- .gemini/commands/om-wrap-up.toml
- .gemini/settings.json
- .gitattributes
- .github/FUNDING.yml
- .github/pull_request_template.md
- .github/scripts/delivery-gate.ts
- .github/scripts/generate-changelog.ts
- .github/scripts/manifest-check.ts
- .github/scripts/package.json
- .github/workflows/manifest-check.yml
- .github/workflows/mod.yml
- .github/workflows/pr-title.yml
- .github/workflows/release.yml
- .github/workflows/test.yml
- .gitignore
- .mcp.json
- .obsidian/app.json
- .scripts/gemini-commands.ts
- .scripts/package.json
- .scripts/qmd-bootstrap.ts
- .shardmind/hooks/bootstrap.ts
- .shardmind/hooks/package.json
- .shardmind/hooks/personalize.ts
- .shardmind/hooks/post-update.ts
- .shardmind/shard-schema.yaml
- .shardmind/shard.yaml
- .shardmindignore
- AGENTS.md
- ARCHITECTURE.md
- ATTRIBUTION.md
- AUTOGINEER-RUNS.md
- CHANGELOG.md
- CONTRIBUTING.md
- Home.md
- LICENSE
- README.ja.md
- README.ko.md
- README.md
- README.zh-CN.md
- bases/1-1 History.base
- bases/Competency Map.base
- bases/Incidents.base
- bases/Memories.base
- bases/People Directory.base
- bases/Recently Touched.base
- bases/Review Evidence.base
- bases/Templates.base
- bases/Work Dashboard.base
- brain/Gotchas.md
- brain/Key Decisions.md
- brain/Memories.md
- brain/North Star.md
- brain/Patterns.md
- brain/Skills.md
- obsidian-mind-demo.gif (not served here)
- obsidian-mind-logo.png (not served here)
- org/People & Context.md
- org/people/.gitkeep
- org/teams/.gitkeep
- perf/Brag Doc.md
- perf/brag/.gitkeep
- perf/competencies/README.md
- perf/evidence/.gitkeep
- reference/.gitkeep
- templates/Competency Note.md
- templates/Decision Record.md
- templates/Review Template.md
- templates/Thinking Note.md
- templates/Work Note.md
- thinking/README.md
- vault-manifest.json
- work/1-1/.gitkeep
- work/Index.md
- work/active/.gitkeep
- work/archive/.gitkeep
- work/incidents/.gitkeep
- work/meetings/README.md
Advantages
- Provides persistent memory across sessions, preventing agents from starting from zero. evidence: README.md
- Uses local semantic search for efficient, offline retrieval of information. evidence: README.md
- Automates vault hygiene and organization through lifecycle hooks. evidence: README.md
Limitations
- Requires manual setup of hooks and optional dependencies like QMD for full functionality. evidence: README.md
- Semantic search indexing can be resource-intensive on machines without a dedicated GPU. evidence: README.md
- Hook scripts rely on experimental Node.js flags that may change in future versions. evidence: README.md
Security
Risk level: High risk
This brain runs
Agent instructions that run or fetch something (82)
.claude/agents/brag-spotter.md:`git log --since="<quarter start>" --oneline` on the vault itself.claude/agents/correction-sweep.md:Run both..claude/agents/review-prep.md:**Git History** (optional): `git log --since="<start>" --until="<end>" --oneline` for volume of vault activity during the period..claude/agents/vault-librarian.md:Run a full health check and produce a maintenance report..claude/agents/vault-librarian.md:**Orphan Detection**: Run `obsidian orphans` (or `grep -rL '\[\[' work/ org/ perf/ brain/ reference/` as fallback)..claude/agents/vault-librarian.md:**Broken Wikilinks**: Run `obsidian unresolved` (or grep for `\[\[.*\]\]` patterns and check if targets exist)..claude/agents/vault-migrator.md:**Task**: Execute the migration plan — read from source, transform, write to target..claude/commands/om-correct.md:Run it **at the moment of correction**, not on a schedule..claude/commands/om-humanize.md:The user can run `git diff` for the full picture..claude/commands/om-humanize.md:If charcount matters (review content in `perf/`), verify limits after editing with `node --experimental-strip-types .claude/scripts/charcount.ts`..claude/commands/om-incident-capture.md:"Should I run `/om-vault-audit` to verify everything links properly?".claude/commands/om-intake.md:No arguments needed — just drop files and run..claude/commands/om-intake.md:Run this command after..claude/commands/om-intake.md:Before routing, run `qmd query "<meeting topic or person name>"` to find existing notes the content should attach to..claude/commands/om-meeting.md:**Search the vault** — run `qmd query "$ARGUMENTS"` and `qmd vsearch "$ARGUMENTS"` to find everything related to the topic, including semantically related content.claude/commands/om-prep-1on1.md:**Check open tasks** — run `obsidian tasks daily todo`;.claude/commands/om-prep-1on1.md:Run `/om-capture-1on1 $ARGUMENTS` to file the notes into `work/1-1/` and update the person note..claude/commands/om-project-archive.md:git mv "work/active/<Note>.md" "work/archive/YYYY/".claude/commands/om-project-archive.md:git mv "work/active/<Topic>" "work/archive/YYYY/<Topic>".claude/commands/om-project-archive.md:Run a quick check that no wikilinks are broken (Obsidian resolves by name, so moves shouldn't break links).claude/commands/om-review-peer.md:PR evidence from `perf/evidence/<Name> PRs - <cycle>.md` (run `/om-peer-scan` if missing).claude/commands/om-review-peer.md:Use `node --experimental-strip-types .claude/scripts/charcount.ts <file> "<section>" "" <limit>` to verify..claude/commands/om-review-peer.md:Run these before presenting the draft:.claude/commands/om-review-peer.md:[ ] All sections within your tool's character limit (use `node --experimental-strip-types .claude/scripts/charcount.ts <file> "<section>" "" <limit>`).claude/commands/om-self-review.md:[ ] All sections within your tool's character limit (use `node --experimental-strip-types .claude/scripts/charcount.ts <file> "<section>" "" <limit>`).claude/commands/om-standup.md:Run the morning standup..claude/commands/om-standup.md:Re-reading `Home.md`, `brain/North Star.md`, or re-running `git log` doubles the token cost for zero new information..claude/commands/om-tidy.md:Run on demand, during wrap-up when flags are present, or when the flag list has visibly piled up..claude/commands/om-tidy.md:**Completed-not-archived** — `git mv` from `work/active/` to `work/archive/YYYY/`;.claude/commands/om-tidy.md:`node --experimental-strip-types .claude/scripts/tidy-fix.ts [--apply]` runs the DETERMINISTIC subset from cron with no agent: archives completed work, migrates misplaced memory files (copy → regenerate index → verify → remove), and refuses every judgment class with a pointer back here..claude/commands/om-tidy.md:Dry-run by default..claude/commands/om-vault-audit.md:If a note is in the wrong folder, move it with `git mv`.claude/commands/om-vault-upgrade.md:/om-vault-upgrade --dry-run # show the upgrade plan only.claude/commands/om-vault-upgrade.md:/om-vault-upgrade <path-to-source-vault> --dry-run.claude/commands/om-vault-upgrade.md:/om-vault-upgrade --source <path-to-source-vault> [--dry-run].claude/commands/om-vault-upgrade.md:`--dry-run` — produce the plan without changing anything.claude/commands/om-vault-upgrade.md:upgrade with `npm i -g shardmind@latest`)..claude/commands/om-vault-upgrade.md:node --experimental-strip-types .claude/scripts/upgrade-plan.ts.claude/commands/om-vault-upgrade.md:**Managed** (`.shardmind/state.json` exists): it reads `shardmind --json` and `shardmind update --dry-run --json`..claude/commands/om-vault-upgrade.md:It reads `shardmind adopt … --from-version <v> --dry-run --yes --json` and hashes each differing file against that release..claude/commands/om-vault-upgrade.md:With `--dry-run`, stop here..claude/commands/om-vault-upgrade.md:ShardMind executes only through its terminal UI (`--json` works with `--dry-run` only), so run it non-interactively: `CI=true … --yes < /dev/null`..claude/commands/om-vault-upgrade.md:**Conflicts:** ask the user to run `!.claude/commands/om-vault-upgrade.md:A managed update deletes an untouched `CLAUDE.md` or `GEMINI.md` in that same run;.claude/commands/om-vault-upgrade.md:node --experimental-strip-types .claude/scripts/upgrade-plan.ts --apply-behind.claude/commands/om-vault-upgrade.md:**Conflicts:** have the user run `!.claude/commands/om-vault-upgrade.md:The hooks run: `echo '{}' | node --experimental-strip-types .claude/scripts/session-start.ts` exits 0 and prints the session context, as one JSON object whose `hookSpecificOutput.additionalContext` carries it..claude/commands/om-vault-upgrade.md:**Not found**: run version fingerprint detection from the target's `vault-manifest.json`:.claude/commands/om-vault-upgrade.md:Run Tier 0 (vault shape detection) to understand the organizational pattern.claude/commands/om-vault-upgrade.md:On re-run, compute the **source hash** for each source file and compare against the logged source hash:.claude/commands/om-vault-upgrade.md:Say 'go' to execute.".claude/commands/om-vault-upgrade.md:**For `--dry-run`**: write the plan to `thinking/vault-upgrade-plan-YYYY-MM-DD.md` and stop..claude/commands/om-vault-upgrade.md:⚠ 3 orphan notes (no inbound links) — run /om-vault-audit to fix.claude/commands/om-vault-upgrade.md:"Run `/om-vault-audit` for a full structural check".claude/commands/om-weekly.md:**`brag-spotter`** — run with weekly scope to find uncaptured wins and competency gaps.claude/commands/om-weekly.md:`git log --since="7 days ago" --oneline --no-merges` — all vault changes.claude/commands/om-weekly.md:Run the `brag-spotter` subagent with its standard (quarterly) scope, then filter its findings down to wins that clearly occurred in the past 7 days..claude/commands/om-wrap-up.md:**`brag-spotter`** — run at the end to find uncaptured wins and competency gaps from the session.claude/commands/om-wrap-up.md:Auto-memory index — if brain/ notes were added or their descriptions changed, REGENERATE the index instead of hand-editing it: `node --experimental-strip-types .claude/scripts/generate-memory-index.ts > <your ~/.claude/projects/<slug>/memory/MEMORY.md path>`..claude/skills/defuddle/SKILL.md:If not installed: `npm install -g defuddle`.claude/skills/excalidraw-diagram/SKILL.md:When generating an Excalidraw diagram, **automatically execute these steps:**.claude/skills/excalidraw-diagram/SKILL.md:1. Open https://excalidraw.com· uses the network.claude/skills/excalidraw-diagram/SKILL.md:1. Open https://dai-shi.github.io/excalidraw-animate/· uses the network.claude/skills/obsidian-bases/SKILL.md:| Function | Signature | Description | |----------|-----------|-------------| | `date()` | `date(string): date` | Parse string to date (`YYYY-MM-DD HH:mm:ss`) | | `now()` | `now(): date` | Current date and time | | `today()` | `today(): date` | Current date (time = 00:00:00) | | `if()` …[+244 chars]· uses the network.claude/skills/obsidian-cli/SKILL.md:Run `obsidian help` to see all available commands..claude/skills/obsidian-cli/SKILL.md:Run JavaScript in the app context:.claude/skills/obsidian-cli/SKILL.md:Run `obsidian help` to see additional developer commands including CDP and debugger controls..claude/skills/qmd/SKILL.md:Use when the MCP server isn't registered (older vault clones, Windows install drift, disabled in `.mcp.json`), or for one-off shell checks outside a session..claude/skills/qmd/SKILL.md:The template ships `qmd_index` empty so every install gets its own store without being asked a question..claude/skills/qmd/SKILL.md:The SessionStart hook and the PostToolUse debounced refresh both run this automatically;.claude/skills/qmd/SKILL.md:`qmd --index <name> embed` — Regenerate vector embeddings (slower, run after bulk changes)..claude/skills/qmd/SKILL.md:Run the bootstrap once:.claude/skills/qmd/SKILL.md:node --experimental-strip-types .scripts/qmd-bootstrap.ts.claude/skills/qmd/SKILL.md:Idempotent — safe to re-run..claude/skills/qmd/SKILL.md:Run `qmd --index <name> update && qmd --index <name> embed` in the shell..claude/skills/qmd/SKILL.md:The SessionStart hook and the PostToolUse debounced refresh run `update` automatically, but `embed` should be run explicitly after sessions that create many notes.AGENTS.md:Stale Actives), `Recently Touched` (recency by real mtime — the answer to "what's most recent", not filename dates), `Incidents`, `People Directory`, `1-1 History`, `Review Evidence`, `Competency Map`, `Templates` | | `work/` | Work notes index | `Index.md` (detailed MOC) | | `work/ac …[+1219 chars]AGENTS.md:See `.claude/rules/obsidian-cli.md`, or run `obsidian help`.AGENTS.md:Codex will find and execute the command file.AGENTS.md:On a fresh clone, run `node --experimental-strip-types .scripts/qmd-bootstrap.ts` once to build the index.AGENTS.md:Run `/om-standup`.AGENTS.md:**Zero data loss**: when reorganizing, always use `git mv`.
Hooks (14)
.claude/settings.json(SessionStart):node --disable-warning=ExperimentalWarning --experimental-strip-types "$(r="${CLAUDE_PROJECT_DIR:-.}"; d="$r"; while [ ! -f "$d/vault-manifest.json" ] && p="$(dirname "$d")" && [ "$d" != "$p" ]; do d="$p"; done; [ -f "$d/vault-manifest.json" ] || d="$r"; printf %s "$d")/.claude/scripts/ …[+17 chars].claude/settings.json(UserPromptSubmit):node --disable-warning=ExperimentalWarning --experimental-strip-types "$(r="${CLAUDE_PROJECT_DIR:-.}"; d="$r"; while [ ! -f "$d/vault-manifest.json" ] && p="$(dirname "$d")" && [ "$d" != "$p" ]; do d="$p"; done; [ -f "$d/vault-manifest.json" ] || d="$r"; printf %s "$d")/.claude/scripts/ …[+20 chars].claude/settings.json(PostToolUse):node --disable-warning=ExperimentalWarning --experimental-strip-types "$(r="${CLAUDE_PROJECT_DIR:-.}"; d="$r"; while [ ! -f "$d/vault-manifest.json" ] && p="$(dirname "$d")" && [ "$d" != "$p" ]; do d="$p"; done; [ -f "$d/vault-manifest.json" ] || d="$r"; printf %s "$d")/.claude/scripts/ …[+18 chars].claude/settings.json(PreCompact):node --disable-warning=ExperimentalWarning --experimental-strip-types "$(r="${CLAUDE_PROJECT_DIR:-.}"; d="$r"; while [ ! -f "$d/vault-manifest.json" ] && p="$(dirname "$d")" && [ "$d" != "$p" ]; do d="$p"; done; [ -f "$d/vault-manifest.json" ] || d="$r"; printf %s "$d")/.claude/scripts/ …[+15 chars].claude/settings.json(Stop):node --disable-warning=ExperimentalWarning --experimental-strip-types "$(r="${CLAUDE_PROJECT_DIR:-.}"; d="$r"; while [ ! -f "$d/vault-manifest.json" ] && p="$(dirname "$d")" && [ "$d" != "$p" ]; do d="$p"; done; [ -f "$d/vault-manifest.json" ] || d="$r"; printf %s "$d")/.claude/scripts/ …[+18 chars].codex/hooks.json(SessionStart):node --disable-warning=ExperimentalWarning --experimental-strip-types "${CODEX_PROJECT_DIR:-.}/.claude/scripts/session-start.ts".codex/hooks.json(UserPromptSubmit):node --disable-warning=ExperimentalWarning --experimental-strip-types "${CODEX_PROJECT_DIR:-.}/.claude/scripts/classify-message.ts".codex/hooks.json(PostToolUse):node --disable-warning=ExperimentalWarning --experimental-strip-types "${CODEX_PROJECT_DIR:-.}/.claude/scripts/validate-write.ts".codex/hooks.json(Stop):node --disable-warning=ExperimentalWarning --experimental-strip-types "${CODEX_PROJECT_DIR:-.}/.claude/scripts/stop-checklist.ts".gemini/settings.json(SessionStart):node --disable-warning=ExperimentalWarning --experimental-strip-types .claude/scripts/session-start.ts.gemini/settings.json(BeforeAgent):node --disable-warning=ExperimentalWarning --experimental-strip-types .claude/scripts/classify-message.ts.gemini/settings.json(AfterTool):node --disable-warning=ExperimentalWarning --experimental-strip-types .claude/scripts/validate-write.ts.gemini/settings.json(PreCompress):node --disable-warning=ExperimentalWarning --experimental-strip-types .claude/scripts/pre-compact.ts.gemini/settings.json(SessionEnd):node --disable-warning=ExperimentalWarning --experimental-strip-types .claude/scripts/stop-checklist.ts
MCP servers (2)
.mcp.json(qmd):node .claude/scripts/qmd-mcp.mjsREADME.md(om):node /absolute/path/to/your-vault/.claude/scripts/om-mcp.mjs
- Medium risk: runs-instruction-instruction-network (.claude/skills/excalidraw-diagram/SKILL.md:456)
- Medium risk: runs-instruction-instruction-network (.claude/skills/excalidraw-diagram/SKILL.md:470)
- Medium risk: runs-instruction-instruction-network (.claude/skills/obsidian-bases/SKILL.md:181)
- Low risk: network-instructions (.claude/commands/om-vault-upgrade.md:30)
- Low risk: scripts-present (.claude/scripts/classify-message.ts)
- Low risk: scripts-present (.claude/scripts/lib/active-hygiene.ts)
- Low risk: scripts-present (.claude/scripts/lib/atomic-write.ts)
- Low risk: scripts-present (.claude/scripts/lib/correction-sweep.ts)
- Low risk: scripts-present (.claude/scripts/lib/env-path.ts)
- Low risk: scripts-present (.claude/scripts/lib/frontmatter.ts)
- Low risk: scripts-present (.claude/scripts/lib/hint-state.ts)
- Low risk: scripts-present (.claude/scripts/lib/hook-io.ts)
- Low risk: scripts-present (.claude/scripts/lib/instruction-files.ts)
- Low risk: scripts-present (.claude/scripts/lib/main-guard.ts)
- Low risk: scripts-present (.claude/scripts/lib/matcher.ts)
- Low risk: scripts-present (.claude/scripts/lib/mcp-exposure.ts)
- Low risk: scripts-present (.claude/scripts/lib/mcp-qmd-client.ts)
- Low risk: scripts-present (.claude/scripts/lib/memory-promoted.ts)
- Low risk: scripts-present (.claude/scripts/lib/memory-recall.ts)
- Low risk: scripts-present (.claude/scripts/lib/memory-supersede.ts)
- Low risk: scripts-present (.claude/scripts/lib/memory-write.ts)
- Low risk: scripts-present (.claude/scripts/lib/om-mod.ts)
- Low risk: scripts-present (.claude/scripts/lib/project-dir.ts)
- Low risk: scripts-present (.claude/scripts/lib/prose-width.ts)
- Low risk: scripts-present (.claude/scripts/lib/qmd-cwd.ts)
- Low risk: scripts-present (.claude/scripts/lib/qmd-refresh.ts)
- Low risk: scripts-present (.claude/scripts/lib/qmd.ts)
- Low risk: scripts-present (.claude/scripts/lib/read-head.ts)
- Low risk: scripts-present (.claude/scripts/lib/regex.ts)
- Low risk: scripts-present (.claude/scripts/lib/reinject.ts)
- Low risk: scripts-present (.claude/scripts/lib/report-key.ts)
- Low risk: scripts-present (.claude/scripts/lib/session-start-run.ts)
- Low risk: scripts-present (.claude/scripts/lib/session-start.ts)
- Low risk: scripts-present (.claude/scripts/lib/signals.ts)
- Low risk: scripts-present (.claude/scripts/lib/stop-handoff.ts)
- Low risk: scripts-present (.claude/scripts/lib/stop-report.ts)
- Low risk: scripts-present (.claude/scripts/lib/write-targets.ts)
- Low risk: scripts-present (.claude/scripts/pre-compact.ts)
- Low risk: scripts-present (.claude/scripts/qmd-mcp.mjs)
- Low risk: scripts-present (.claude/scripts/session-start.ts)
- Low risk: scripts-present (.claude/scripts/stop-checklist.ts)
- Low risk: scripts-present (.claude/scripts/validate-write.ts)
- Low risk: network-instructions (.claude/skills/defuddle/SKILL.md:10)
AutoGineer scanned breferrari/obsidian-mind@fae7be4 (fae7be4).
Attribution and licence
- First committed by
breferrari
- Declared authors
- Steph Ango (declared in .claude-plugin/plugin.json)
- Publisher
- Brenno Ferrari (Community)
- Source
- breferrari/obsidian-mind · commit fae7be4
- Licence
- MIT License (files served here)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Contributors
- breferrari, Yi-111-a, sedetweiler, A220man, HoangThai18
AutoGineer is not affiliated with or endorsed by breferrari.
Install from source
3 of its files are not served here, so there is no ZIP. breferrari/obsidian-mind at fae7be4