Three YouTube videos landed on my feed in the same week, all making some version of the same argument: your Obsidian second brain is useless. Static knowledge in the store, dynamic data behind a connector, tree not graph, retrieval not visualization. I watched them expecting to be smug — I have crons, I have skills, I have MCP servers wired into the vault. This is a real system, not a Zettelkasten larp.
Then I ran the numbers.
The trigger
My vault has 1,060 markdown files. 976 of them have zero inbound [[wikilinks]] — a 92% orphan rate. Daily/ is 512 files and 1.9 MB, roughly half the vault by weight, and my skills reference it exactly 3 times versus 40 references to Projects/. Five months of daily capture with the retrieval side never built.
Those numbers were bad enough that I opened a project called Vault Rot Audit and started looking for what was rotting.
The interesting part is that almost none of what I found was the thing I expected. The orphan rate turned out to be a non-issue — this vault is navigated by folder hierarchy and generated index views, not by graph. Adding 900 backlinks would have been busywork that fixed nothing. The real problem was one order of magnitude weirder.
The audit that had been auditing itself
I have a systemd unit called pa-docs-audit that runs every Sunday at 5:45 AM. It’s specced to do two things:
- Regenerate the
_Active/_Paused/_Ideas/_Clientsindex views inside the vault. - Drop findings — orphans, dead files, broken refs, undocumented timers — into
Inbox/audit-YYYY-MM-DD.md.
Step 1 works fine. My index views were current.
Step 2 also works, and that’s worse.

Fourteen weekly reports written. Zero survive. Three months of a detector firing perfectly and nobody — no human, no agent, nothing — ever reading the output.
The loop
The deleter is a step in another skill: pagoodmorning, which runs at 5:05 AM every morning as part of my briefing. Step 1 of the skill says, in plain English:
Check
Inbox/for any files beyondREADME.mdand theReferences/folder. If found, triage them before proceeding.
And the audit report’s own last line, written by the audit itself, was:
Triage and delete when handled.
So the audit wrote its findings into the one folder that gets auto-swept every morning, and every finding it wrote carried an instruction telling the sweeper to delete it. The Sunday audit ran, wrote a report full of “here are the things you should fix,” and by Monday morning the report had been read by an LLM briefing, marked as “triaged,” and removed.
The detector fired every week for three months. The consumer ate the alarm before I could read it.
— my Vault Rot Audit doc, 2026-08-11
There is nothing wrong with the audit. There is nothing wrong with the sweeper. They are both doing exactly what they were told to do. The bug is in the shape of the loop.
The fix that broke the signal it was fixing
Once I understood the loop, the fix was mechanical. Findings now write to Projects/_Audit.md (latest, always overwritten) plus a dated history in _meta/audit/ that keeps twelve runs. The Triage and delete when handled line — the instruction that invited its own deletion — is gone. And pagoodmorning step 1 now refuses to sweep any file with generated: true or type: audit in the frontmatter, so the two skills have an explicit contract instead of an accidental one.

Then I built the second half of the audit: enforcement. Not just “here are 40 projects that haven’t been touched in 45 days,” but “these are now marked paused automatically, and the Now-focus board has been rebalanced.” Same script, same audit, but with an --apply flag wired into the timer.
I ran a dry run. It would have paused four live client hubs, because a client isn’t dormant just because the note is. I added an exemption for domain: client files. I ran another dry run. It would have paused three files in Ideas/, which is a sacred folder that never gets touched by enforcement, because ideas sitting for a year is the point of Ideas/. Exempt.
Third dry run passed. I flipped it live.
And the enforcement was quietly erasing the signal that had triggered it.
The mtime bug
Here’s the bug. My audit derives the “days idle” for each project from the file’s mtime. So if a project hasn’t been touched in 79 days, its mtime says 79 days ago, and the audit correctly flags it.
Writing status: paused into the frontmatter of that file, with a normal file write, resets the mtime to now. So the moment enforcement acted on a stale project — the moment it recorded 79 days of neglect by marking it paused — the file’s mtime jumped to 0 days idle. The next week’s audit would look at the same file, see it was worked on this week, and treat it as freshly active.
Enforcement was silently laundering neglect into freshness.
I caught it because I looked at 27 files whose mtimes had jumped to the same second, right after the first --apply run. That was a suspicious cluster. The set_frontmatter_field() helper now preserves mtime, and I restored the 27 corrupted timestamps from a local snapshot I’d taken before the first apply. (I take a local tarball snapshot before any bulk vault change now. My cloud “backup” is rclone sync — that’s a mirror, not an archive. Deletions propagate within 24 hours. If I destroy a file at noon, I have until tomorrow noon to notice before the “backup” is a copy of my mistake.)
The pattern, if there is one
Both bugs — the audit-eats-itself loop and the mtime-erases-neglect bug — share a shape. In both cases every individual piece was correct: the audit correctly identified findings, the sweeper correctly triaged the inbox, the enforcement correctly wrote frontmatter, the “days idle” query correctly read mtime. Nothing was doing the wrong thing.
The system was doing the wrong thing. That is a different debug.
- 2026-05-12First audit ran cleanReport written to Inbox/audit-2026-05-12.md at 05:00. Deleted at 05:05 by the morning briefing.
- 2026-05 → 2026-0813 more reportsSame story every Sunday. No error, no warning, no queue growth to hint that anything was wrong.
- 2026-08-10Ran the numbers on the vaultDiscovered the orphan rate, the daily-notes-never-read gap, the Now-cap violation, the 14 missing reports.
- 2026-08-11Fixed the loopFindings moved out of Inbox/, sweeper made refuse to delete generated files, enforcement --apply wired in.
- 2026-08-11Caught the mtime bugEnforcement's first apply reset 27 files to mtime=now. Restored from tarball snapshot. Helper fixed to preserve mtime.
The lesson I’m carrying out of this — beyond the specific --preserve-mtime and the specific “detector output frontmatter marks it non-swept” — is that when I write a skill in isolation, I am usually confident it’s correct. When I wire it into an existing set of skills that share a filesystem, I am not evaluating the skill anymore. I am evaluating the loop it just closed. And loops are much harder to reason about from the inside of any single script.
The audit I wrote was correct. It also produced zero signal for three months because I had never asked “and then what happens to the report?” I had asked “did the audit run?” and gotten a yes.
What I’m not doing
The videos say to move to Notion, rebuild for graph traversal, and prune aggressively. I rejected all three.
Notion is aimed at teams of humans who need pretty pages. I am one operator with a filesystem-native blog pipeline, git sync, and 76 skills that pattern-match on paths. Markdown is the API here.
The graph isn’t load-bearing, so the 92% orphan rate is a fake signal. What matters is “reachable from any generated index view,” and that’s a much smaller number.
And deleting Daily/ — the biggest weight in the vault — would destroy the only irreplaceable asset I have. Five months of daily context is not a folder to prune. It is inventory to index. That’s a whole separate build.
The takeaway
If you run any kind of automated audit or health check, ask two questions, in this order:
- Where does the output go?
- What else touches that place, and what does it do there?
If the answer to (2) is “another script I wrote, which reads the folder and cleans it up,” you have this bug. It doesn’t matter how carefully the audit was written or how good the findings are. The audit is a tree falling in a forest with a very tidy groundskeeper on staff.
The fix is boring — separate the write location from any auto-swept location, and mark generated files as protected. The interesting part is the failure mode itself: two correct scripts, sharing an implicit integration point, producing exactly zero output for three months without anyone noticing. That’s what silent failure looks like in a system full of green checkmarks.
Green checkmarks are not the same as signal. I keep learning this one.