v8.3.0
Every bug in this release had passing tests over it.
Not "insufficient coverage" — passing, specific, deliberate tests, several of them written to prove the exact property that turned out to be false. Six of them shipped in v8.2.0 and were found the next day, and the thing they have in common is worth more than any individual fix:
Each test's fixture was simpler than production, in the one dimension that mattered.
An exposure policy tested against ["brain", "projects"] while the shipped manifest declares work/active/, perf/brag/, org/people/ — so a check comparing the first path segment passed everything, and refused most of a real vault. A guard normalised in its constructor, tested by a suite that builds policies by hand — so the normalisation never ran in the test and the guard failed open. A block-addressing routine tested on single-line bullets, while the documentation tells you to anchor multi-line ones.
The two that found real damage were both cases where a fixture was replaced with a copy of a real file: a genuine vault-manifest.json, and a genuine brain/ note. Nothing else changed.
v8.3.0 is what that produced: one security fix, two silent data-exposure bugs, a feature that finally works, and a rewrite of the part that three rounds of patching could not fix.
Quick Start
Requirements: Obsidian, Node 22+ LTS, and one of: Claude Code · Codex CLI · Gemini CLI
Install via ShardMind (recommended — wizard, optional modules, managed upgrades):
npm install -g shardmind
mkdir my-vault && cd my-vault
shardmind install github:breferrari/obsidian-mindOr clone directly:
git clone https://github.com/breferrari/obsidian-mind.git
cd obsidian-mind
npm install -g @tobilu/qmd && qmd index && qmd embed # optional: semantic searchOpen the folder as an Obsidian vault, then run claude, codex, or gemini in the directory and type /om-standup (or om-standup in Codex).
Or download the vault zip below and extract it into your Obsidian vaults folder.
Already using obsidian-mind? Just tell your agent: Update this vault to the latest obsidian-mind from https://github.com/breferrari/obsidian-mind
🔌 Enabling the MCP server (one command)
Set up once per machine, not per repo. Nothing here touches the vault itself.
claude mcp add --scope user om node "/absolute/path/to/your-vault/.claude/scripts/om-mcp.mjs"That covers every directory on the machine, with nothing added to any repository. No environment variable either: the launcher resolves the vault from its own location.
Get the path with cd /path/to/your-vault && pwd.
The per-repo .mcp.json form is still supported, for when you want a specific repo to carry the wiring so a teammate gets it on clone:
{
"mcpServers": {
"om": {
"command": "node",
"args": ["/absolute/path/to/your-vault/.claude/scripts/om-mcp.mjs"]
}
}
}Warning
Use an absolute path, and don't copy the relative one. This vault's own .mcp.json registers qmd with a relative path, which is correct there because a session in the vault is already in the right directory. Reused for om in another project, the same shape silently resolves against that project instead, and the server never starts.
Step 2 is the one that matters, and it is not optional. The consuming repo needs a short section in its own CLAUDE.md telling it the vault exists and when to reach for it. Written out in full in the README — copy it and fill in the triggers.
Measured, and the reason this step is not paperwork: 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 server's own instructions propagates reliably; a positive "go and look" 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.
v8.2.0 adds one clause to that snippet — see Changed, below.
What's new in v8.3.0 — 2026-07-31
Fixed
-
mcp_never_exposewithheld only the spelling it was declared in. The admit side has always compared filenames case-insensitively —isExposedPath,matchedRoot— because the filesystem does. The withhold side was exact-case. On Windows and macOS that asymmetry is a bypass: withmcp_never_expose: ["SOUL.md"],vault://note/brain/SOUL.mdwas refused andvault://note/brain/soul.mdreturned the file.resources/listomitted it,resources/readserved it — the enumerator withholding while the resolver hands it over, which is the two-surfaces-disagree defect this module exists to prevent, inverted.realpathSyncon Windows does not canonicalise case, so the post-realpath re-check saw the caller's spelling rather than the file's and missed it too. Found by copying a real vault's manifest into a fixture instead of inventing one; measured against that vault, six spellings served before the fix, none after. The first attempt is the instructive part: lowercasing the set insideresolveExposurecloses the manifest path and leaves every hand-built policy failing open — which is what every test constructs, so the verification probe went green while the hole stayed. The comparison owns the case rule now, so it holds however the policy was assembled. (#185) -
A
private:marker past the frontmatter head window read as public.readHeadstops at 1200 characters, soisPrivatewas inspecting a prefix of the frontmatter rather than the frontmatter: a note with enoughaliases:to pushprivate: truepast that point was served onsearch,expandandresources/listalike. The obvious fix is worse than the bug and was shipped once before being caught — withholding any note whose frontmatter does not close inside the prefix fails closed, and measured against a real vault it made two ordinary public notes vanish from every read surface, silently, with the affected set growing as frontmatter grows. Guessing public is the bug; guessing private is an outage. It looks further now, and withholds only when the block does not close at all. (#187) -
record_workrefused every multi-segment exposed root.resolveDestinationcomparedrel.split("/")[0]against a root list whose entries are path prefixes, so a vault declaringwork/activerefusedfolder: "work/active"— with the message "'work' is not an exposed root (allowed: brain, work/active)", naming the root it had just refused. That is not an edge case:mcp_exposed_rootsships empty, so a default install derives its roots fromuser_content_roots, and after normalisation the shipped manifest iswork/active,work/archive,work/1-1,org/people,perf/brag— nearly all multi-segment. On a fresh install thefolderargument was refused for every root a user would file into, and theinbox/fallback caught it, which is why it read as a routing quirk rather than a bug. (#184) -
A capture could not be linked by the name it calls itself.
record_workwrites a Related section listing sibling notes, and every entry rendered as_(no note yet)_— including references to notes written hours earlier in the same run. Thirteen consecutive captures arrived mutually unlinked, in a vault whose stated rule is that a note without links is a bug. The filename is a slug of the title: lowercased, punctuation stripped, cut at 60 characters, date-prefixed.resolvableNamesmatches basenames and aliases, and a capture had neither spelling of its own name. Worth being precise about, because the truncation is the loud part and not the whole of it — the date prefix and the lowercasing break the round trip on their own, so every capture was unreachable by title, not only the long-titled ones. The title now ships as an alias. (#180) -
Frontmatter quoting broke on a Windows path or a newline. Values were written into double-quoted YAML scalars, which process escapes — so a title carrying
C:\temp\xis a parse error (\tis a TAB,\xwants two hex digits), and this repo's own fixtures deliberately containC:\paths. A newline ended the scalar at a column-zero continuation. Either one takes down the whole frontmatter block rather than its own field. Both now go through a single-quoted scalar, where the only escape is''and a backslash is a backslash. The reader had the mirror-image bug:resolvableNames"unquoted" a value by stripping one leading and one trailing quote character, which is not the same operation as undoing the quoting —'It''s here'would have resolved asIt''s here, a name no wikilink can match and one an author reading the note would swear is present. (#181)
Added
-
recallserves the promoted block itself. A lesson promoted from a capture into abrain/topic note used to exist twice and be reachable once:recallread only the memory root,search/expandread everything but, and a session in another repo could only ever get the capture — the version as first written, which may predate a correction swept through the promoted one. It now serves the promoted block, so the foreign repo reads the corrected text.The design turns on one fact that only became true in v8.2.0: promotion is additive, so the capture never leaves. The capture is therefore still the reach record and is already correct, and nothing about
scopeorprojectshad to migrate onto an ordinary note. Visibility is computed exactly as before; only the content changes.Four things bound it. Serving is opt-in — only an anchored marker serves, so every capture promoted before this is untouched until someone re-points it. The exposure policy still decides, through the same predicate every other surface asks, so a
privateor never-exposed note is refused here too. A stale anchor degrades to the capture rather than widening to the whole note. And what is served is capped, at 40 lines or 8,000 characters, with the caller told when the cap bit.The block finder was rebuilt rather than patched. The original classified a line — one pattern for an id at the end of one, another for an id alone — then walked outward to guess the block's extent. Which block a line belongs to is not a property of that line, so the walks compensated for information the design never computed, and the two branches encoded opposite assumptions: three audit rounds each patched one and the next found the same defects alive in the branch left untouched. Over 90 enumerated cases it answered 11 wrong; the segmenting version answers 0, and is 6.6× faster because the fence mask stops being rebuilt per anchor. The defect that mattered was not exotic —
/om-tidytells you to put the id at the end of a bullet, and the finder assumed the id's line was the block's first, so a two-line bullet served one line and reported it complete. (#188, #179) -
recallsays when a memory has a corrected version it cannot serve. For markers that carry no anchor, or point somewhere withheld, or whose anchor has gone stale, the entry names the promoted note and says the promoted text wins on conflict — rather than serving the older capture as though nothing had superseded it.
Changed
- One named predicate answers "may this path be read", and every surface asks it.
resolveExposedNoteis extracted from the resource resolver with no behaviour change.visibleFilesalready answered which notes exist for every enumerating surface; this gives the other question the same single answer. The reason to name it is that this layer has now grown a second read path twice, and both times the second path applied a different rule — first the symlink case, then a hand-rolled root check that droppedneverExpose, droppedisPrivate, and matched first segments against prefixes. (#186)
Upgrading
No action required. Every fix is a correction to existing behaviour, and promoted-block serving is opt-in: a capture with a bare promoted: <note> marker behaves exactly as it did in v8.2.0.
One habit changes, and it is the only thing worth doing deliberately:
Important
Anchor your promotion markers, and check what the anchor points at.
v8.2.0 asked you to add promoted: "<note>" when copying a capture into brain/. That still clears the hygiene flag and still serves nothing. To have the corrected text actually reach the repo it was written for, point the marker at a block:
promoted: "brain/Gotchas - Engineering#^om-a1b2c3"Give the promoted block an Obsidian block id — at the end of the bullet or paragraph, preceded by a space, or alone on the line directly after it. A heading anchor (#Some Section) works too, but a level-1 heading is refused, since in most vaults the H1 is the note's own title and anchoring there would address the whole note.
Two things to check before adding one, because that block, and nothing around it, is what a session in another repo receives: it should read correctly on its own to someone outside this vault, and the target note must be one the server actually serves — anchoring into a private or never-exposed note is a no-op, not a leak.
shardmind update # if you installed via ShardMindOr tell your agent: Update this vault to the latest obsidian-mind from https://github.com/breferrari/obsidian-mind
Full history: CHANGELOG.md · Previous release: v8.2.0