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
.github/scripts/delivery-gate.ts
Shown as plain text.
#!/usr/bin/env node
/**
* Delivery gate (#267): ask the model what it received, never the logs.
*
* Every check the template had confirmed that a hook RAN. None confirmed what
* ARRIVED, and from Claude Code 2.1.89 to the #254 fix, hook output past
* 10,000 characters reached the model as a 2,000-character preview while
* every log said success. The mod moves the session context to a channel
* (an instruction file) on a young API, so the same failure is possible
* again. This gate runs real Claude Code sessions in throwaway vaults and, at
* each checkpoint, asks the model to quote two things from the context it was
* given: its LAST line (the injection meter), and a marker token placed in a
* brain note in its MIDDLE. A context cut anywhere loses one of them.
*
* - with the mod: at startup, in a session of its own; then in a second
* session after `/compact`, from a general-purpose subagent straight after
* `/clear`, and after a second `/clear`;
* - without the mod (the settings hooks alone): at startup, after `/compact`
* and after `/clear`. The hook path's context is held under the hook cap by
* the template itself, so there it collapses the brain index to a pointer
* and only the last line is asked for.
*
* With the mod, each `/compact` and `/clear` first changes the fixture: one
* more brain note moves the delivered size, and the marker is rewritten. So
* the context delivered afterwards ends with a size, and carries a marker,
* that nobody has quoted yet: neither a compaction summary nor an earlier
* answer can supply them, only a fresh delivery can.
*
* Only the stream decides, and every other road an answer could take is
* closed in the judge, not in the prompt. A checkpoint is INVALID, never PASS,
* when: its turn used a tool; the `/compact` summary carried the new size or
* marker, or could not be read; a `/compact` or `/clear` left no event of its
* own; the subagent was not a general-purpose one, was handed the size or
* marker in its prompt, used tools, or did not report its tool count; with the
* mod, the settings hook printed the context instead of standing down (or,
* without it, printed nothing); or the session errored, timed out, ran an
* extra turn or ended early.
*
* Run it by hand on any Claude Code version before it is recommended, with a
* logged-in `claude`:
*
* node --experimental-strip-types .github/scripts/delivery-gate.ts [--trusted-dir <dir>] [--file-issue] [--hooks-only] [--model <full model id>] [--claude <bin>]
*
* `--trusted-dir` loads the mod the way a user gets it: found in the vault's
* `.claude/skills/` after the workspace-trust prompt, which a headless session
* cannot accept. Give it an empty folder you have opened once in Claude Code
* and trusted; the gate builds each session's vault in that exact folder
* (trust does not extend to subfolders) and empties it afterwards. Without it,
* the mod is loaded with `--plugin-dir` from a temp-folder vault.
*
* `--file-issue` opens a GitHub issue (with `gh`) when a checkpoint fails.
*
* `--hooks-only` runs only the sessions without the mod (the settings hooks
* alone), for a change that touches the hook path and not the mod's (#340).
* It cannot be combined with `--self-test`, which only runs the mod.
*
* `--self-test` runs two broken copies of the mod instead: one cuts its
* context to 2,000 characters everywhere, one delivers it whole only at
* startup. Every checkpoint must FAIL, except the startup-only copy's own
* startup, which must PASS (the control that proves that copy delivered).
*
* Exit codes: 0 pass (with `--self-test`: the gate can fail); 1 a checkpoint
* did not receive the current context (with `--self-test`: the gate is
* broken); 2 the run could not be judged. A FAIL wins over an INVALID, and in
* the self-test so does any PASS against a broken mod.
*
* It costs model turns on the caller's account, and it is not wired to CI: a
* public repository's CI has no credentials to give it.
*/
import { spawn, spawnSync } from "node:child_process";
import { randomBytes } from "node:crypto";
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parseHookContext } from "../../.claude/scripts/lib/hook-io.ts";
import { isMainModule } from "../../.claude/scripts/lib/main-guard.ts";
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
/** Claude Code's cap on a hook's output, and the preview it hands the model past it (#254). */
const HOOK_CAP_CHARS = 10_000;
const PREVIEW_CHARS = 2_000;
/** How far past the hook cap the mod's context must be, so a cut near the cap shows. */
const FIXTURE_MIN_CHARS = Math.round(HOOK_CAP_CHARS * 1.2);
/** Each session's spending cap, passed to `claude --max-budget-usd`. */
const SESSION_BUDGET_USD = "5";
/** How long one turn may take before the session is abandoned. */
const TURN_TIMEOUT_MS = 5 * 60_000;
/**
* The model the gate asks for when `--model` isn't given: a full id, because a
* bare alias is not honoured for every model and can silently run another
* (#346). Update it when a newer model ships.
*/
export const DEFAULT_MODEL = "claude-opus-5-5";
/** What a checkpoint's answer must contain: the context's last line, and the middle marker when the context carries one. */
export type Expected = { readonly line: string; readonly marker: string | null };
/** The prefix of the marker token placed in the middle of the context. */
export const MARKER_PREFIX = "GATE-MARK-";
/** The question asked at every main-loop checkpoint. */
export const QUESTION = [
"Without using any tool, reply with two things from the obsidian-mind session context you were given, verbatim:",
"(1) its last line, the one that starts with _context injected;",
`(2) the token in it that starts with ${MARKER_PREFIX}.`,
"Write NONE for any you cannot see.",
].join(" ");
/** The task the subagent must be given, word for word. */
export const SUBAGENT_TASK = [
"Without using any tool, reply with two things from your own context or instructions, verbatim:",
"(1) the line that starts with _context injected;",
`(2) the token that starts with ${MARKER_PREFIX}.`,
"Write NONE for any you cannot see.",
].join(" ");
/** The same question, put to a general-purpose subagent; only its own report is judged. */
export const SUBAGENT_QUESTION = [
'Use the Agent tool to start one general-purpose subagent (subagent_type "general-purpose") with exactly this task and nothing else:',
`'${SUBAGENT_TASK}'`,
"Then reply with the subagent's answer verbatim and nothing else.",
].join(" ");
/** A first turn that puts nothing about the context into the conversation. */
export const WARM_UP = "Reply with the single word OK.";
/** The kinds of step whose answer is judged; the rest prepare the next one. */
const CHECKPOINT_KINDS = ["ask", "ask-subagent"] as const;
type CheckpointKind = (typeof CHECKPOINT_KINDS)[number];
type PreparingKind = "warm" | "compact" | "clear";
export type StepKind = CheckpointKind | PreparingKind;
/**
* One turn of a session. A named step is a checkpoint; the others prepare the
* next one. A step that shifts (every `/compact` and `/clear`) changes the
* fixture just before it is sent, so the context delivered from then on ends
* with a line nobody has seen yet.
*/
export type Step = { readonly name: string; readonly kind: StepKind; readonly prompt: string };
/** Whether a step starts a new delivery of the context: every `/compact` and `/clear`. */
export const shifts = (step: Step): boolean => step.kind === "compact" || step.kind === "clear";
/** Whether a step's answer is judged: the questions, not the turns that prepare them. */
export const isCheckpoint = (step: Step): boolean => (CHECKPOINT_KINDS as readonly string[]).includes(step.kind);
const prepare = (kind: PreparingKind, prompt: string): Step => ({ name: "", kind, prompt });
const ask = (name: string): Step => ({ name, kind: "ask", prompt: QUESTION });
/**
* The sessions a run is made of, each with the turns it sends. The subagent
* is asked straight after `/clear`, before the main loop has quoted anything
* in that conversation, so there is nothing for it to be handed; the second
* `/clear` clears the subagent's answer away before the main loop is asked.
*/
export function plan(withMod: boolean): ReadonlyArray<readonly Step[]> {
if (!withMod) {
return [
[ask("without the mod, at startup")],
[
prepare("warm", WARM_UP),
prepare("compact", "/compact"),
ask("without the mod, after /compact"),
prepare("clear", "/clear"),
ask("without the mod, after /clear"),
],
];
}
return [
[ask("at startup")],
[
prepare("warm", WARM_UP),
prepare("compact", "/compact"),
ask("after /compact"),
prepare("clear", "/clear"),
{ name: "from a subagent", kind: "ask-subagent", prompt: SUBAGENT_QUESTION },
prepare("clear", "/clear"),
ask("after /clear"),
],
];
}
/** The last non-empty line of a text, trimmed. */
export function lastLine(text: string): string {
return (
text
.split(/\r?\n/)
.map((l) => l.trim())
.filter((l) => l !== "")
.pop() ?? ""
);
}
/** Whether an answer quotes `expected`, ignoring quotes, backticks, emphasis marks and spacing a model may add or drop. */
export function quotes(answer: string, expected: string): boolean {
const bare = (s: string) =>
s
.replace(/[`"“”*_]/g, "")
.replace(/\s+/g, " ")
.trim();
return bare(expected) !== "" && bare(answer).includes(bare(expected));
}
/** Whether an answer gives everything a checkpoint expects: the last line, and the marker when there is one. */
export function answers(answer: string, want: Expected): boolean {
return quotes(answer, want.line) && (want.marker === null || answer.includes(want.marker));
}
/** The delivered size on a context's meter line (`15.2` from `_context injected: 15.2kB / ...`), or undefined. */
export const meterSize = (line: string): string | undefined => line.match(/(\d+(?:\.\d+)?)\s*kB/i)?.[1];
/**
* Whether a meter line says the context gave anything up to fit its budget:
* a section collapsed to its pointer, or held at a lower level (#304).
*/
const collapsed = (line: string): boolean => line.includes("collapsed") || line.includes("degraded");
/**
* Whether a text carries enough to rebuild what a checkpoint expects: the
* whole line, its delivered size as a number on its own (`15.2`, whatever unit
* follows; not the budget, which stays the same when the context changes), or
* the marker. Used where none of it may travel (a compaction summary, a
* subagent's prompt); stricter than `quotes` on purpose, since there a false
* alarm costs a rerun and a miss costs a false PASS.
*/
export function carries(text: string, want: Expected): boolean {
if (quotes(text, want.line)) return true;
if (want.marker !== null && text.includes(want.marker)) return true;
const size = meterSize(want.line);
return size !== undefined && new RegExp(`(?<![\\d.])${size.replace(".", "\\.")}(?![\\d])`).test(text);
}
/** One Agent call: what the main loop asked for, and what came back. */
export type AgentCall = {
readonly type: string | null;
/** Everything the main loop handed the subagent: its description and its prompt. */
readonly prompt: string;
/** The subagent's own words, or null when no result came back. */
readonly report: string | null;
/** Tools the subagent used, or null when the result did not say. */
readonly toolUses: number | null;
};
/** What one turn of a stream-json transcript shows, up to and including its `result` event. */
export type Turn = {
/** The main loop's own text. */
readonly text: string;
/** Tools the main loop called, by name. */
readonly tools: readonly string[];
readonly agentCalls: readonly AgentCall[];
/** Tool uses seen in subagent messages streamed inline, if any. */
readonly subagentToolUses: number;
readonly compacted: boolean;
/** The compaction summary the model wrote, when this turn compacted. */
readonly summaries: readonly string[];
readonly reset: boolean;
/** What each settings SessionStart hook printed in this turn. */
readonly sessionStartOutputs: readonly string[];
/** Set when the turn ended in an error rather than a result. */
readonly error: string | null;
};
type Block = {
type?: string;
text?: string;
name?: string;
id?: string;
tool_use_id?: string;
content?: unknown;
input?: { prompt?: string; description?: string; subagent_type?: string };
};
type StreamEvent = {
type?: string;
subtype?: string;
hook_event?: string;
stdout?: string;
isSynthetic?: boolean;
parent_tool_use_id?: string | null;
message?: { content?: Block[] | string };
tool_use_result?: { content?: unknown; totalToolUseCount?: number; agentType?: string };
is_error?: boolean;
result?: string;
model?: string;
};
const textOf = (content: unknown): string =>
typeof content === "string" ? content : Array.isArray(content) ? content.map((b: Block) => (b.type === "text" ? (b.text ?? "") : "")).join("\n") : "";
/**
* The subagent's own words from a framed Agent tool_result: what follows the
* hand-back frame's "The report follows:" line, up to the `agentId:` footer
* at the start of a line. Used only when the result carries no raw content.
*/
export function subagentReport(handedBack: string): string {
const start = handedBack.indexOf("The report follows:");
if (start < 0) return handedBack.trim();
const body = handedBack.slice(start + "The report follows:".length);
const end = body.search(/\nagentId:/);
return (end < 0 ? body : body.slice(0, end)).trim();
}
/**
* The models a session's stream says ran: the `model` of each `system`/`init`
* event, once each, in order. Read so the gate can say what actually answered:
* a bare alias is not honoured for every model, and a mismatch would judge
* delivery on one model while the run is labelled with another (#346).
*/
export function modelsRan(streamJson: string): string[] {
const seen: string[] = [];
for (const line of streamJson.split("\n")) {
let event: StreamEvent;
try {
event = JSON.parse(line);
} catch {
continue;
}
if (event.type === "system" && event.subtype === "init" && typeof event.model === "string" && !seen.includes(event.model)) {
seen.push(event.model);
}
}
return seen;
}
/** Whether every session reported running the model asked for. */
export function ranAsAsked(asked: string, ran: readonly string[]): boolean {
return ran.length > 0 && ran.every((m) => m === asked);
}
/** The line that says which model answered, and flags one that isn't the model asked for. */
export function modelNote(asked: string, ran: readonly string[]): string {
if (ran.length === 0) return `Could not tell which model answered (asked for ${asked}).`;
const other = ran.filter((m) => m !== asked);
return other.length === 0
? `Model that answered: ${asked}.`
: `⚠️ Asked for ${asked}, but the sessions ran ${ran.join(", ")}: these verdicts are for ${other.join(", ")}.`;
}
/** Split a stream-json transcript into turns, one per `result` event. Events before the first result belong to the first turn. */
export function parseTurns(streamJson: string): Turn[] {
const turns: Turn[] = [];
const fresh = () => ({
text: "",
tools: [] as string[],
calls: new Map<string, { -readonly [K in keyof AgentCall]: AgentCall[K] }>(),
subagentToolUses: 0,
compacted: false,
summaries: [] as string[],
reset: false,
sessionStartOutputs: [] as string[],
});
let t = fresh();
for (const raw of streamJson.split("\n")) {
let event: StreamEvent;
try {
event = JSON.parse(raw) as StreamEvent;
} catch {
continue;
}
const blocks = Array.isArray(event.message?.content) ? event.message.content : [];
if (event.type === "assistant") {
for (const block of blocks) {
if (event.parent_tool_use_id) {
// A subagent's own message, streamed inline: its tools count against it, its text is not the parent's.
if (block.type === "tool_use") t.subagentToolUses++;
} else if (block.type === "text") t.text += `${block.text ?? ""}\n`;
else if (block.type === "tool_use") {
t.tools.push(block.name ?? "?");
if (block.name === "Agent" && block.id) {
const prompt = [block.input?.description, block.input?.prompt].filter(Boolean).join("\n");
t.calls.set(block.id, { type: block.input?.subagent_type ?? null, prompt, report: null, toolUses: null });
}
}
}
} else if (event.type === "user") {
if (event.isSynthetic) t.summaries.push(textOf(event.message?.content));
for (const block of blocks) {
const call = block.type === "tool_result" && block.tool_use_id ? t.calls.get(block.tool_use_id) : undefined;
if (!call) continue;
const result = event.tool_use_result;
call.report = result?.content !== undefined ? textOf(result.content).trim() : subagentReport(textOf(block.content));
call.toolUses = typeof result?.totalToolUseCount === "number" ? result.totalToolUseCount : null;
call.type = result?.agentType ?? call.type;
}
} else if (event.type === "system" && event.subtype === "compact_boundary") t.compacted = true;
else if (event.type === "system" && event.subtype === "hook_response" && event.hook_event === "SessionStart")
t.sessionStartOutputs.push(event.stdout ?? "");
else if (event.type === "conversation_reset") t.reset = true;
else if (event.type === "result") {
const error = event.is_error || event.subtype !== "success" ? `${event.subtype ?? "error"}: ${(event.result ?? "").slice(0, 200)}` : null;
turns.push({ ...t, text: t.text.trim(), agentCalls: [...t.calls.values()], error });
t = fresh();
}
}
return turns;
}
export type Outcome = "PASS" | "FAIL" | "INVALID";
export type Verdict = { readonly checkpoint: string; readonly expected: string; readonly answer: string; readonly outcome: Outcome; readonly why: string };
/** How a session ended, for checkpoints it never reached. */
export type Ending = "complete" | "timeout" | "extra-turn";
/** A checkpoint's expectation given as just a line (no marker) or in full. */
type ExpectedInput = string | Expected;
const asExpected = (e: ExpectedInput | undefined): Expected =>
e === undefined ? { line: "", marker: null } : typeof e === "string" ? { line: e, marker: null } : e;
/** Why the subagent step cannot be judged, or null when it can. */
function subagentProblem(turn: Turn, want: Expected): string | null {
const calls = turn.agentCalls;
const call = calls[0];
if (calls.length !== 1 || call === undefined || call.report === null) return "no single subagent answered: the main loop answered itself";
if (turn.tools.some((name) => name !== "Agent")) return `the main loop used tools (${turn.tools.join(", ")})`;
if (call.type !== "general-purpose") return `the subagent was ${call.type ?? "of no stated type"}, not general-purpose`;
if (carries(call.prompt, want)) return "the main loop handed the line or marker to the subagent in its prompt or description";
if (call.toolUses === null) return "the subagent's tool count was not reported";
if (call.toolUses > 0 || turn.subagentToolUses > 0) return `the subagent used ${Math.max(call.toolUses, turn.subagentToolUses)} tool(s)`;
return null;
}
/**
* Judge one session against what its context carries. A checkpoint is PASS or
* FAIL only when the stream shows the answer could have come from the
* delivered context alone; anything else is INVALID, with the reason.
*/
export function judge(
steps: readonly Step[],
turns: readonly Turn[],
expected: ExpectedInput | readonly ExpectedInput[],
options: { withMod: boolean; ending?: Ending },
): Verdict[] {
// One expectation for the whole session, or one per step when the fixture shifts during it.
const expectedAt = (i: number): Expected => asExpected(Array.isArray(expected) ? (expected as readonly ExpectedInput[])[i] : (expected as ExpectedInput));
const ending = options.ending ?? "complete";
const verdicts: Verdict[] = [];
// Who delivered: with the mod, every settings SessionStart must have stood down; without it, the first must have printed.
const outputs = turns.flatMap((turn) => turn.sessionStartOutputs);
let sessionProblem: string | null = null;
if (options.withMod && outputs.some((out) => out.trim() !== "")) sessionProblem = "the settings hook printed the context: the mod did not deliver it";
if (!options.withMod && !(turns[0]?.sessionStartOutputs ?? []).some((out) => out.trim() !== ""))
sessionProblem = "the settings hook printed nothing at startup";
// Every sent step yields exactly one result (2.1.288, /compact and /clear included); more means a turn nobody sent, and every pairing after it is wrong.
if (turns.length > steps.length) sessionProblem ??= "the session ran a turn nobody sent";
let carried: string | null = null; // a preparing step that failed spoils the checkpoint after it
steps.forEach((step, i) => {
const turn = turns[i];
const want = expectedAt(i);
let problem = sessionProblem ?? carried;
if (!turn)
problem ??=
ending === "timeout" ? "the turn timed out" : ending === "extra-turn" ? "the session ran a turn nobody sent" : "the session ended before this turn";
else if (turn.error) problem ??= `the turn failed (${turn.error})`;
else if (step.kind === "compact" && !turn.compacted) problem ??= "/compact left no compact_boundary event";
else if (step.kind === "compact" && turn.summaries.every((summary) => summary.trim() === ""))
problem ??= "the /compact summary could not be read, so it cannot be cleared of the line";
else if (step.kind === "compact" && turn.summaries.some((summary) => carries(summary, want)))
problem ??= "the /compact summary itself carried the line or marker";
else if (step.kind === "clear" && !turn.reset) problem ??= "/clear left no conversation_reset event";
else if (step.kind === "ask" && turn.tools.length > 0) problem ??= `the answer used tools (${turn.tools.join(", ")})`;
else if (step.kind === "ask-subagent") problem ??= subagentProblem(turn, want);
if (!isCheckpoint(step)) {
carried = problem;
return;
}
carried = null;
const answer = step.kind === "ask-subagent" ? (turn?.agentCalls[0]?.report ?? "") : (turn?.text ?? "");
if (problem === null && answer.trim() === "") problem = "no answer";
const hit = answers(answer, want);
const shown = hit ? want.line : lastLine(answer);
const verdict = { checkpoint: step.name, expected: want.line + (want.marker ? ` + ${want.marker}` : ""), answer: shown };
if (problem !== null) verdicts.push({ ...verdict, outcome: "INVALID", why: problem });
else if (hit) verdicts.push({ ...verdict, outcome: "PASS", why: "" });
else
verdicts.push({
...verdict,
outcome: "FAIL",
why: failReason(
answer,
want,
steps.slice(0, i).map((_, j) => expectedAt(j)),
),
});
});
return verdicts;
}
/** What a FAIL looked like: the tail without the middle, a context from before a shift, or nothing recognisable. */
function failReason(answer: string, want: Expected, earlier: readonly Expected[]): string {
if (quotes(answer, want.line) && want.marker !== null) return "the middle of the context did not arrive: the last line did, the marker did not";
const stale = earlier.some(
(e) => e.line !== "" && e.line !== want.line && (quotes(answer, e.line) || (e.marker !== null && e.marker !== want.marker && answer.includes(e.marker))),
);
return stale ? "stale: it quoted the context from before the shift" : "";
}
/** Brain notes added to the fixture: enough to put the mod's context well past the hook cap and under its instruction budget. */
const FIXTURE_NOTES = 110;
/** The note that carries the marker: the middle of the brain index, which is the middle of the context. */
const MARKER_NOTE = 55;
/** Ways the self-test breaks a copy of the mod. */
export type Breakage = "none" | "cut" | "startup-only";
const DELIVERY = "await update($, sessionContext, () => text)";
const BROKEN: Record<Exclude<Breakage, "none">, string> = {
cut: `await update($, sessionContext, () => text.slice(0, ${PREVIEW_CHARS}))`,
"startup-only": `await update($, sessionContext, () => (e.source === 'startup' ? text : text.slice(0, ${PREVIEW_CHARS})))`,
};
/** A fresh marker token. */
export const newMarker = (): string => `${MARKER_PREFIX}${randomBytes(4).toString("hex").toUpperCase()}`;
/** A brain note with a description and a short body, as the fixture writes them. */
function brainNote(title: string, description: string): string {
return `---\ndescription: "${description}"\ntags:\n - brain\n---\n\n# ${title}\n\n${"Body. ".repeat(20)}\n`;
}
/** Write the brain note that carries the marker, in the middle of the index. */
function writeMarkerNote(vault: string, marker: string): void {
const n = String(MARKER_NOTE).padStart(3, "0");
writeFileSync(
join(vault, "brain", `Gate Rule ${n}.md`),
brainNote(`Gate Rule ${n}`, `Standing rule ${n}, holding the marker ${marker} in the middle of the brain index`),
);
}
/** The file that marks a folder as the gate's own, so it is never asked to empty anything else. */
const FIXTURE_MARK = ".om-delivery-gate";
/** Every fixture this run created, so an interrupted run can still remove them. */
const fixtures = new Map<string, { trusted: boolean }>();
/**
* Empty a trusted folder for the next fixture. It must be empty or the gate's
* own (it holds the mark file): the gate never clears a folder it did not make.
*/
function emptyTrustedDir(dir: string): void {
if (!existsSync(dir)) throw new Error(`--trusted-dir ${dir} does not exist; create it, open it once in Claude Code and trust it`);
const entries = readdirSync(dir);
if (entries.length > 0 && !entries.includes(FIXTURE_MARK))
throw new Error(`--trusted-dir ${dir} is not empty and not the gate's own; give it an empty folder`);
for (const entry of entries) if (entry !== FIXTURE_MARK) rmSync(join(dir, entry), { recursive: true, force: true, maxRetries: 10, retryDelay: 500 });
writeFileSync(join(dir, FIXTURE_MARK), "This folder belongs to the obsidian-mind delivery gate, which empties it before and after each session.\n");
}
/**
* A throwaway vault: the template's files (tracked and new) plus brain notes,
* with the mod broken as asked and `marker` in the middle note. It is built in
* a fresh folder under the OS temp folder, or in `trustedDir` itself.
* `withMod: false` turns the repo-shipped mod off for the vault, so a trusted
* folder runs the settings hooks alone.
*/
export function buildFixture(
breakage: Breakage,
options: { marker?: string; trustedDir?: string | undefined; withMod?: boolean; notes?: number } = {},
): string {
const { marker = newMarker(), trustedDir, withMod = true, notes = FIXTURE_NOTES } = options;
let vault: string;
if (trustedDir === undefined) vault = mkdtempSync(join(tmpdir(), "om-delivery-gate-"));
else {
emptyTrustedDir(trustedDir);
vault = trustedDir;
}
fixtures.set(vault, { trusted: trustedDir !== undefined });
const listed = spawnSync("git", ["-C", REPO, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8" }).stdout;
for (const file of new Set(listed.split("\0").filter(Boolean))) {
if (!existsSync(join(REPO, file))) continue; // tracked but deleted locally
mkdirSync(dirname(join(vault, file)), { recursive: true });
cpSync(join(REPO, file), join(vault, file));
}
// A vault copied from infrastructure alone has no brain/ of its own.
mkdirSync(join(vault, "brain"), { recursive: true });
for (let i = 1; i <= notes; i++) {
const n = String(i).padStart(3, "0");
if (i === MARKER_NOTE) continue;
writeFileSync(
join(vault, "brain", `Gate Rule ${n}.md`),
brainNote(`Gate Rule ${n}`, `Standing rule ${n}, one of many that together push the brain index past the hook-output cap`),
);
}
if (notes >= MARKER_NOTE) writeMarkerNote(vault, marker);
if (breakage !== "none") {
const register = join(vault, ".claude/skills/obsidian-mind/hooks/register.ts");
const source = readFileSync(register, "utf8");
const broken = source.replace(DELIVERY, BROKEN[breakage]);
if (broken === source) throw new Error(`self-test: the ${breakage} breakage did not land; the mod changed shape`);
writeFileSync(register, broken);
}
if (!withMod) {
writeFileSync(join(vault, ".claude/settings.local.json"), JSON.stringify({ enabledPlugins: { "obsidian-mind@skills-dir": false } }, null, 2) + "\n");
}
spawnSync("git", ["init", "-q"], { cwd: vault });
return vault;
}
/** The folders Claude Code keeps for a session run in `vault`: its project transcripts and its temp task folder. */
export function sessionDirs(vault: string, env: NodeJS.ProcessEnv = process.env, real: (path: string) => string = realOrSame): string[] {
// The path as created and as resolved (macOS's /var is /private/var): Claude Code may slug either.
const slugs = [...new Set([vault, real(vault)].map((path) => path.replace(/[^A-Za-z0-9-]/g, "-")))].filter((slug) => slug.includes("om-delivery-gate-"));
return slugs.flatMap((slug) => [join(env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"), "projects", slug), join(tmpdir(), "claude", slug)]);
}
function realOrSame(path: string): string {
try {
return realpathSync(path);
} catch {
return path;
}
}
/** Remove a temp fixture and its session folders, or empty a trusted one (keeping the folder and its trust). */
export function removeFixture(vault: string): void {
try {
if (fixtures.get(vault)?.trusted) emptyTrustedDir(vault);
else for (const path of [vault, ...sessionDirs(vault)]) rmSync(path, { recursive: true, force: true, maxRetries: 10, retryDelay: 500 });
} catch (error) {
console.warn(`Could not clean up ${vault}: ${(error as Error).message}`);
}
fixtures.delete(vault);
}
/** The caller's Claude Code variables a fixture session still needs: where config lives, how to reach a shell and how to authenticate. */
const KEPT_CLAUDE_VARIABLES = new Set([
"CLAUDE_CONFIG_DIR",
"CLAUDE_CODE_GIT_BASH_PATH",
"CLAUDE_CODE_OAUTH_TOKEN",
"CLAUDE_CODE_USE_BEDROCK",
"CLAUDE_CODE_USE_VERTEX",
]);
/**
* The environment every process in the fixture runs with. The caller's other
* Claude Code variables (a gate run from inside a session inherits them) and
* NODE_PATH are dropped. npm's global prefix points at an empty folder, so qmd
* does not resolve through it and session-start.ts skips its search
* bootstrap: otherwise each throwaway vault starts a full qmd bootstrap and
* embed that outlives the run and registers an index on the caller's machine.
*/
export function fixtureEnv(vault: string, from: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
const kept = Object.entries(from).filter(
([key]) => KEPT_CLAUDE_VARIABLES.has(key.toUpperCase()) || (!/^CLAUDE/i.test(key) && key.toUpperCase() !== "NODE_PATH"),
);
return { ...Object.fromEntries(kept), npm_config_prefix: join(vault, ".gate-npm"), DISABLE_AUTOUPDATER: "1" };
}
/** What session-start.ts prints in the fixture for a start of `source`, as the hook or as the mod's run (`deliver`). */
export function contextOf(vault: string, deliver: boolean, source: "startup" | "compact" | "clear" = "startup"): string {
const run = spawnSync(
process.execPath,
["--disable-warning=ExperimentalWarning", "--experimental-strip-types", join(vault, ".claude/scripts/session-start.ts")],
{
cwd: vault,
input: JSON.stringify({ source, ...(deliver ? { om_mod: "deliver" } : {}) }),
encoding: "utf8",
env: { ...fixtureEnv(vault), CLAUDE_PROJECT_DIR: vault },
},
);
if (run.status !== 0) throw new Error(`session-start.ts failed: ${run.stderr}`);
if (deliver) return run.stdout;
// The hook path prints one JSON envelope (#340): the context is its
// additionalContext, which is what reaches the model.
return parseHookContext(run.stdout, "SessionStart");
}
/** What a context makes a checkpoint expect: its last line, and the marker if the context carries it. */
export function expectedOf(context: string, marker: string): Expected {
return { line: lastLine(context), marker: context.includes(marker) ? marker : null };
}
/**
* Change the fixture so the context delivered from now on carries what nobody
* has quoted: one more brain note moves the delivered size, and the middle
* note gets a new marker. Returns the new expectation; throws if the size did
* not move or the context collapsed, either of which would leave the
* checkpoints after it unable to tell fresh delivery from memory.
*/
export function shiftFixture(vault: string, n: number, before: Expected): Expected {
writeFileSync(
join(vault, "brain", `Gate Shift ${n}.md`),
brainNote(`Gate Shift ${n}`, `Added mid-session (shift ${n}) so the context ends with a size nobody has quoted yet; ${"padding ".repeat(30)}`),
);
const marker = newMarker();
writeMarkerNote(vault, marker);
const after = expectedOf(contextOf(vault, true), marker);
if (meterSize(after.line) === undefined || meterSize(after.line) === meterSize(before.line))
throw new Error(`shift ${n} did not move the delivered size: ${after.line}`);
if (collapsed(after.line)) throw new Error(`shift ${n} pushed the context past the instruction budget, so it collapsed: ${after.line}`);
if (after.marker === null) throw new Error(`shift ${n}: the new marker is not in the delivered context`);
return after;
}
/**
* Called once per step, just before it is sent: what that step's answer must
* give. The starting expectation until a step starts a new delivery; from
* then on what `next` returned for it, so a shift lands before the step that
* causes the re-delivery, never earlier.
*/
export function expectationTracker(start: Expected, next: (n: number, before: Expected, step: Step) => Expected): (step: Step) => Expected {
let current = start;
let count = 0;
return (step) => {
if (shifts(step)) current = next(++count, current, step);
return current;
};
}
/**
* The `result` events in the lines a chunk completes, and the line still
* arriving. Counting as lines complete keeps pacing linear in the transcript,
* and an event split across two chunks is counted once, when its line ends.
*/
export function countResults(pending: string, chunk: string): { results: number; pending: string } {
const lines = (pending + chunk).split("\n");
const rest = lines.pop() ?? "";
let results = 0;
for (const line of lines) {
try {
if ((JSON.parse(line) as { type?: string }).type === "result") results++;
} catch {
/* not an event line */
}
}
return { results, pending: rest };
}
/** How one session is run. */
type SessionOptions = { readonly claude: string; readonly model: string; readonly withMod: boolean; readonly trustedDir?: string | undefined };
/** Run one paced session: each turn is sent once the previous one's result arrives, after `beforeSend` has run for it. */
function session(
vault: string,
steps: readonly Step[],
options: SessionOptions,
beforeSend: (step: Step, index: number) => void = () => {},
): Promise<{ transcript: string; ending: Ending }> {
return new Promise((resolvePromise, reject) => {
const args = ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose", "--model", options.model];
// The main loop has the Agent tool and nothing else: an answer cannot come from reading the vault.
args.push("--tools", "Agent", "--allowedTools", "Agent", "--setting-sources", "project,local", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}');
args.push("--max-budget-usd", SESSION_BUDGET_USD);
// In a trusted folder the mod is found the way a user's is; elsewhere it is loaded by path.
if (options.withMod && options.trustedDir === undefined) args.push("--plugin-dir", join(vault, ".claude/skills/obsidian-mind"));
const child = spawn(options.claude, args, { cwd: vault, env: fixtureEnv(vault), stdio: ["pipe", "pipe", "pipe"] });
let out = "";
let pending = ""; // the line still arriving
let results = 0;
let sent = 0;
let ending: Ending = "complete";
let exited = false;
let timer: NodeJS.Timeout | undefined;
const stop = (why: Ending) => {
ending = why;
child.kill();
};
const send = () => {
if (exited) return;
const index = sent++;
const step = steps[index];
if (!step) return child.stdin.end();
try {
beforeSend(step, index);
} catch (error) {
child.kill();
reject(error);
return;
}
clearTimeout(timer);
timer = setTimeout(() => stop("timeout"), TURN_TIMEOUT_MS);
child.stdin.write(`${JSON.stringify({ type: "user", message: { role: "user", content: step.prompt } })}\n`);
};
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => {
out += chunk;
const counted = countResults(pending, chunk);
pending = counted.pending;
results += counted.results;
// A turn nobody sent (a plugin's own prompt) would pair every later step with the wrong turn.
if (results > sent) stop("extra-turn");
else if (results === sent) send();
});
child.stdin.on("error", () => {}); // a write to a child that already exited; its close settles the session
child.stderr.resume(); // drained, so a chatty child cannot block on a full pipe
child.on("error", (error: NodeJS.ErrnoException) => {
clearTimeout(timer);
reject(
error.code === "ENOENT" || error.code === "EINVAL"
? new Error(`could not start ${options.claude}; on Windows pass --claude <path to claude.exe>`)
: error,
);
});
child.on("close", () => {
exited = true;
clearTimeout(timer);
resolvePromise({ transcript: out, ending });
});
send();
});
}
/** One fresh fixture per session, checked before any turn is spent on it. */
async function runSession(
steps: readonly Step[],
breakage: Breakage,
options: SessionOptions,
): Promise<{ verdicts: Verdict[]; models: string[] }> {
const marker = newMarker();
const vault = buildFixture(breakage, { marker, trustedDir: options.trustedDir, withMod: options.withMod });
try {
const context = contextOf(vault, options.withMod);
const start = expectedOf(context, marker);
if (options.withMod) {
// Well past the hook cap, so a cut near it shows; whole, not collapsed
// under the instruction budget, or the gate would be checking a
// pointer; and the marker in its middle, so a cut there shows too.
if (context.length <= FIXTURE_MIN_CHARS) throw new Error(`fixture too small to test the cap: ${context.length} characters`);
if (collapsed(start.line)) throw new Error(`fixture past the instruction budget, so the context collapsed: ${start.line}`);
const at = context.indexOf(marker) / context.length;
if (at < 0.25 || at > 0.75) throw new Error(`the marker sits at ${Math.round(at * 100)}% of the context, not in its middle`);
}
// With the mod, a new delivery follows a shifted fixture. Without it, the
// hook prints what session-start gives that start's source.
const next = (n: number, before: Expected, step: Step): Expected =>
options.withMod ? shiftFixture(vault, n, before) : expectedOf(contextOf(vault, false, step.kind === "compact" ? "compact" : "clear"), marker);
const expectFor = expectationTracker(start, next);
const expectedByStep: Expected[] = [];
const { transcript, ending } = await session(vault, steps, options, (step, index) => {
expectedByStep[index] = expectFor(step);
});
// Steps never sent (the session stopped early) are INVALID whatever their expectation.
const verdicts = judge(
steps,
parseTurns(transcript),
steps.map((_, i) => expectedByStep[i] ?? ""),
{ withMod: options.withMod, ending },
);
return { verdicts, models: modelsRan(transcript) };
} finally {
removeFixture(vault);
}
}
const print = (label: string, verdicts: readonly Verdict[]) => {
for (const v of verdicts) {
console.log(`${v.outcome.padEnd(7)} ${v.checkpoint}${label}${v.why ? ` (${v.why})` : ""}\n expected: ${v.expected}\n answered: ${v.answer}`);
}
};
/** Why these flags cannot run together, or null when they can. */
export function flagConflict(argv: readonly string[]): string | null {
return argv.includes("--self-test") && argv.includes("--hooks-only")
? "--self-test breaks the mod, and --hooks-only runs no session with the mod: drop one of them."
: null;
}
/** The self-test's expectation: every checkpoint fails, except the startup-only copy's own startup, its positive control. */
export function selfTestOutcome(runs: ReadonlyArray<{ readonly breakage: Breakage; readonly verdicts: readonly Verdict[] }>): {
exitCode: 0 | 1 | 2;
message: string;
} {
const all = runs.flatMap((run) => run.verdicts.map((v) => ({ ...v, breakage: run.breakage })));
const isControl = (v: { breakage: Breakage; checkpoint: string }) => v.breakage === "startup-only" && v.checkpoint === "at startup";
// A PASS against a broken mod proves the gate cannot fail there; nothing else in the run can excuse it.
const leaked = all.filter((v) => v.outcome === "PASS" && !isControl(v));
if (leaked.length > 0) return { exitCode: 1, message: `The gate is broken: ${leaked.map((v) => `${v.checkpoint} (${v.breakage}) passed`).join(", ")}.` };
const invalid = all.filter((v) => v.outcome === "INVALID").length;
if (invalid > 0) return { exitCode: 2, message: `${invalid} checkpoint(s) could not be judged; fix the run before trusting the self-test.` };
const wrong = all.filter((v) => v.outcome !== (isControl(v) ? "PASS" : "FAIL"));
if (wrong.length > 0) return { exitCode: 1, message: `The gate is broken: ${wrong.map((v) => `${v.checkpoint} (${v.breakage}) ${v.outcome}`).join(", ")}.` };
return { exitCode: 0, message: "The gate can fail: every checkpoint failed against both broken mods, and the startup-only control passed." };
}
/** The issue `--file-issue` opens for a failing run. */
export function issueBody(version: string, model: string, verdicts: readonly Verdict[], trusted: boolean): string {
const rows = verdicts.map((v) => `| ${v.checkpoint} | ${v.outcome} | ${v.why || "-"} |`).join("\n");
const loading = trusted ? "found in a trusted vault, as a user's is (--trusted-dir)" : "loaded with --plugin-dir";
const flags = trusted ? " --trusted-dir <an empty folder you trusted once in Claude Code>" : "";
return [
`The delivery gate failed on Claude Code ${version} (model ${model}, the mod ${loading}): at least one checkpoint did not receive the current session context.`,
"",
"| Checkpoint | Outcome | Why |",
"|---|---|---|",
rows,
"",
"Until this is resolved, do not recommend this Claude Code version. Reproduce with:",
"",
"```bash",
`node --experimental-strip-types .github/scripts/delivery-gate.ts${flags} --model ${model} --self-test`,
`node --experimental-strip-types .github/scripts/delivery-gate.ts${flags} --model ${model}`,
"```",
"",
].join("\n");
}
async function main(): Promise<void> {
const argv = process.argv.slice(2);
const flag = (name: string): string | undefined => {
const i = argv.indexOf(name);
return i >= 0 && argv[i + 1] ? argv[i + 1] : undefined;
};
const conflict = flagConflict(argv);
if (conflict) {
console.error(conflict);
process.exit(2);
}
const selfTest = argv.includes("--self-test");
const hooksOnly = argv.includes("--hooks-only");
const trustedDir = flag("--trusted-dir");
const base = { claude: flag("--claude") ?? "claude", model: flag("--model") ?? DEFAULT_MODEL, ...(trustedDir ? { trustedDir: resolve(trustedDir) } : {}) };
const version = spawnSync(base.claude, ["--version"], { encoding: "utf8" }).stdout?.trim() || "unknown version";
process.on("SIGINT", () => {
for (const vault of [...fixtures.keys()]) removeFixture(vault);
process.exit(130);
});
const modelsSeen = new Set<string>();
const run = async (steps: readonly Step[], breakage: Breakage, withMod: boolean): Promise<Verdict[]> => {
const r = await runSession(steps, breakage, { ...base, withMod });
for (const model of r.models) modelsSeen.add(model);
return r.verdicts;
};
const loading = trustedDir ? `found in the trusted folder ${resolve(trustedDir)}` : "loaded with --plugin-dir";
console.log(`Delivery gate · ${version} · ${base.model} · ${hooksOnly ? "settings hooks only" : `the mod ${loading}`}${selfTest ? " · SELF-TEST" : ""}\n`);
if (selfTest) {
const runs: Array<{ breakage: Breakage; verdicts: Verdict[] }> = [];
for (const breakage of ["cut", "startup-only"] as const) {
const verdicts: Verdict[] = [];
for (const steps of plan(true)) verdicts.push(...(await run(steps, breakage, true)));
print(` [${breakage}]`, verdicts);
runs.push({ breakage, verdicts });
}
const { exitCode, message } = selfTestOutcome(runs);
console.log(`\n${message}`);
console.log(modelNote(base.model, [...modelsSeen]));
process.exitCode = ranAsAsked(base.model, [...modelsSeen]) ? exitCode : 2;
return;
}
const verdicts: Verdict[] = [];
if (!hooksOnly) for (const steps of plan(true)) verdicts.push(...(await run(steps, "none", true)));
for (const steps of plan(false)) verdicts.push(...(await run(steps, "none", false)));
print("", verdicts);
console.log(`\n${modelNote(base.model, [...modelsSeen])}`);
const invalid = verdicts.filter((v) => v.outcome === "INVALID").length;
const failed = verdicts.filter((v) => v.outcome === "FAIL").length;
if (invalid > 0) console.log(`\n${invalid} checkpoint(s) could not be judged.`);
if (failed > 0) console.log(`\n${failed} checkpoint(s) did not receive the current context: cut, missing, or stale from before a shift.`);
if (invalid === 0 && failed === 0) console.log("\nEvery checkpoint received the current context, its middle and its end.");
if (failed > 0 && argv.includes("--file-issue")) {
const bodyFile = join(mkdtempSync(join(tmpdir(), "om-gate-issue-")), "body.md");
writeFileSync(bodyFile, issueBody(version, base.model, verdicts, trustedDir !== undefined));
const filed = spawnSync("gh", ["issue", "create", "--title", `Delivery gate fails on Claude Code ${version}`, "--body-file", bodyFile], {
cwd: REPO,
encoding: "utf8",
});
console.log(filed.status === 0 ? `\nFiled: ${filed.stdout.trim()}` : `\nCould not file the issue: ${filed.stderr.trim()}`);
}
// A FAIL needs nothing else to be judged: it wins over an INVALID elsewhere.
// A run that can't show it ran the model asked for can't be judged either (#346).
process.exitCode = failed > 0 ? 1 : invalid > 0 || !ranAsAsked(base.model, [...modelsSeen]) ? 2 : 0;
}
if (isMainModule(import.meta.url)) {
// Anything that stops the run before a verdict (a fixture check, a missing binary) cannot be judged: exit 2, never 1.
await main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 2;
});
}
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