| name | doctor |
| description | Diagnose vault and plugin health — checks broken links, orphan notes, stale memory/ files, inbox backlog, and plugin config validity. Use when the user asks to check vault health, notices something broken, or wants a system audit — 'run /doctor', 'check my vault', 'something seems off'. Do NOT use for: searching vault content (search directly), processing inbox (use consolidate), or updating the system (use update). |
| schedulable | true |
Doctor
Diagnose the health of your OneBrain vault and plugin configuration. Inspired by brew doctor and npm doctor.
Usage:
/doctor — full check (vault + config + memory)
/doctor --vault — vault content checks only (skips CLI doctor)
/doctor --config — plugin config + CLI doctor only (skips vault content)
/doctor --fix — auto-fix safe issues (CLI fix recipes + skill fixes)
Flag detection: Determine active flags from the user's message. --vault = user mentions vault-only or content health; --config = user mentions config or plugin check; --fix = user explicitly asks to fix or auto-fix. Default (no flags) = run all checks.
Two-source architecture (post-v3.0.0 GA):
- CLI doctor (
onebrain doctor --json) handles the built-in checks, grouped into four sections: Config (onebrain.yml, onebrain.yml-keys, vault-config-migration, legacy-qmd-collection), Vault structure (folders, plugin-files, plugin-cache), Integration (settings-hooks, claude-settings), Index & state (orphan-checkpoints, search). Rust-native, single subprocess call, structured JSON output. (search replaced the v3.3 qmd-embeddings probe; legacy-qmd-collection migrates a top-level qmd_collection → search.collection.)
- Skill checks handle vault-content + state-machine checks the CLI doesn't cover: broken wikilinks, orphan notes, stale memory/ files, MEMORY.md size, inbox backlog, log folder size, scheduler health, pause-thread state, memory health.
The skill merges both into one unified report.
Step 1: Read onebrain.yml
Read onebrain.yml. If it's missing → ⛔ onebrain.yml not found — OneBrain may not be configured correctly. Stop.
Resolve folder variables ([inbox_folder], [projects_folder], etc.) from onebrain.yml or defaults.
Step 2a: Run CLI doctor (skip if --vault flag)
Run onebrain doctor --json (or onebrain doctor --fix --json if --fix is active). Parse the JSON envelope:
{
"ok": true|false,
"summary": {"total": N, "errors": N, "warnings": N, "passing": N},
"checks": [
{"check": "<name>", "status": "ok|warn|error", "message": "...", "details": ["..."], "hint": "..."}
],
"fix": [
{"check": "<name>", "outcome": "fixed|partial|failed|manual", "message": "..."}
]
}
fix is a top-level array, a sibling of checks — not a field inside each check object. It is present only when --fix --json was invoked, and its entries are matched back to checks by the check name.
Status → render emoji: ok → ✅, warn → 🟡, error → 🔴.
Detection logic (run in this order):
-
PATH lookup first. If onebrain is not on PATH (which onebrain / where onebrain empty): emit 🔴 onebrain CLI not installed — hooks will not fire; install via brew install onebrain-ai/onebrain/onebrain (or npm install -g @onebrain-ai/cli) — and skip the rest of CLI doctor. Skill checks still run.
-
Otherwise, parse stdout as JSON regardless of exit code. The CLI is sysexits-compliant: exit 1 is the normal "issues found" path when summary.errors > 0 — JSON is still emitted on stdout and is still the source of truth. Only fall back to "CLI absent" if the parse fails AND stderr contains "command not found" or similar — surface the parse error in one line and fall through to skill checks only.
-
Defensive field handling. Treat details as optional — it may be absent, null, or an empty array; render nothing in those cases. hint is also optional. fix is present only when --fix --json was invoked.
When --fix --json returned fix[], render each entry's outcome under the matching check; the post-fix checks[] reflects state after the fix attempts. The CLI emits exactly four outcomes:
outcome | Render | Meaning |
|---|
fixed | ✓ fixed | Recipe ran cleanly; the issue should be resolved. |
partial | 🟡 partially fixed | Recipe made real progress but part of the issue remains. message itemises both halves — show it. |
failed | ✕ failed | Recipe was attempted and did not finish. message explains why. |
manual | → manual | No automated recipe exists. message is the command for the user to run. Not a failure — do not render it as one. |
manual is common, not exceptional. On a vault with a warm daemon — the normal configuration — the CLI's lex-index check reports "could not verify" as a warning with no automated repair, so manual appears on nearly every --fix run. Treat any outcome string outside this table as unknown: render it verbatim and carry on. Never treat an unrecognised outcome as an error — that would report a healthy vault as broken.
Why pass --json: WebFetch / shell text parsing of onebrain doctor's human format is fragile. --json is the stable contract (frozen for v3.x — see CLI CHANGELOG).
Step 2b: Run skill-only checks (skip if --config flag)
The CLI doctor does NOT cover the following — they remain in the skill:
Broken wikilinks
- Grep
[projects_folder], [areas_folder], [knowledge_folder], [resources_folder], [agent_folder] for \[\[.*?\]\] in .md files.
- Skip wikilinks inside fenced code blocks (between
``` fences), blockquote lines (lines beginning with >), inline code spans (the entire [[...]] enclosed in backticks on that line), or YAML frontmatter (between the leading --- and the closing ---).
- For each wikilink, extract note name: strip
|display text suffix AND #anchor fragment. Preserve original text for accurate replacement.
- Check if a
.md file matching the note name exists anywhere in the vault (case-insensitive), testing BOTH the raw slug AND its _→- normalized form. Flag as unresolved only when neither form matches a vault .md. Record as {broken_link, display_text, anchor, source_file, source_line}.
- Classify each unresolved wikilink into one of two buckets (auto-memory slugs live in Claude's auto-memory store OUTSIDE the vault, so they never resolve in Obsidian — these are structurally distinct from typos and missing notes):
- Build the auto-memory slug set, scoped to the CURRENT vault: derive this vault's project-dir name from the vault root absolute path using Claude Code's encoding — replace every
/ and . with - (e.g. vault /Users/keng/Library/Mobile Documents/iCloud~md~obsidian/Documents/ob-1 → project dir -Users-keng-Library-Mobile-Documents-iCloud~md~obsidian-Documents-ob-1). Glob ~/.claude/projects/<derived-dir>/memory/*.md (on Unix expand $HOME, don't pass a literal ~ to file tools). Take basenames without the .md extension. Scoping to the current vault avoids cross-vault false positives — globbing all projects would pull in unrelated vaults' auto-memory slugs and misclassify links. If the derived project dir is absent, skip classification entirely — leave the unresolved links as a single undifferentiated broken-link list and emit nothing for the buckets below. Do NOT fall back to globbing all projects (that reintroduces cross-vault pollution).
- For each unresolved wikilink, take its slug (strip any
memory/ prefix, |display suffix, and #anchor fragment). Also compute a hyphen-normalized form (_ → -).
- Bucket (VAULT-NOTE-WINS is invariant — a wikilink only reaches this step when NO vault
.md matches its raw slug OR its _→- normalized form, per the resolution check above):
- (raw slug OR normalized slug) ∈ auto-memory slug set → 🔴 auto-memory mislink — points outside the vault; fixable via the de-link pass in Step 4.
- else → 🟡 missing note / typo — report only; needs human judgment (existing behavior).
- The 🔴 bucket must never claim a link that a vault note could satisfy (raw or
_→- normalized). Any link whose raw or normalized slug resolves to a vault .md is NOT 🔴 — it is already resolved (not flagged at all), or, if only a fuzzy near-match exists, it belongs in the 🟡 missing-note / fuzzy-fix bucket (Pass C territory), never in 🔴 auto-de-link.
- Report the two buckets separately under 📁 Vault, each with a count. The 🔴 auto-memory bucket lists
file:line — [[slug]]; the 🟡 missing-note bucket keeps the existing broken-link reporting.
Orphan notes
- Find notes in
[knowledge_folder]/ and [resources_folder]/ with zero inbound wikilinks from any other note. Report only — no auto-fix (linking requires semantic judgment; use /connect).
Stale memory/ files
- If
[agent_folder]/MEMORY.md is absent: 🟡 MEMORY.md not found — run /onboarding (skip this + MEMORY.md size check).
- If
memory/ folder is absent: skip.
- Read
memory/ files with status: active or needs-review (skip deprecated).
- Flag:
verified: older than 90 days OR no verified: field OR (conf: low AND verified: absent/older than 30 days).
MEMORY.md size
- Count lines in
[agent_folder]/MEMORY.md. Warn if > 180: suggest pruning Critical Behaviors.
Inbox backlog
- Count
[inbox_folder]/*.md. Warn if > 10: suggest /consolidate.
Old unmerged checkpoints (>7 days)
- The CLI's
orphan-checkpoints check uses an active-session threshold (max(60min, 2 * checkpoint.minutes)) — anything younger may still be a live session.
- The skill check is complementary: glob
[logs_folder]/checkpoint/*-checkpoint-*.md, keep files whose date prefix is older than 7 days. If any → 🟡 suggest /wrapup for the stragglers. (Pre-v2.2.0 vaults may contain merged: true field — ignore it; any file present is unmerged by definition.)
07-logs structure
- Verify the 4 subfolders exist under
[logs_folder]/: session/, checkpoint/, update/, log/. If [logs_folder]/YYYY/MM/ still contains legacy log files: 🟡 legacy log structure — run /update to migrate (and skip the per-subfolder warnings).
- Otherwise warn per missing subfolder: 🟡
07-logs/<name>/ missing — first <type> will create it. No warning when all 4 present.
Log folder size (housekeeping)
- Count
[logs_folder]/log/YYYY/ for the current year. Warn if > 1000: 🟡 log/ folder: N files in YYYY — consider archive (move stale log/YYYY/MM/ folders to 06-archive/ manually). User decides retention; OneBrain has no automatic policy. Skip silently if log/ doesn't exist.
onebrain.yml recap: block (the CLI's onebrain.yml-keys schema check covers required keys; recap: may be optional)
- If
recap: block is absent: 🟡 recap: block missing — /recap will use defaults (min_sessions: 6, min_frequency: 2); run /update to add it.
Plugin install path (CLI plugin-files validates structural integrity but not the install-path registry)
- Read
$HOME/.claude/plugins/installed_plugins.json (Unix) or $env:USERPROFILE/.claude/plugins/installed_plugins.json (Windows PowerShell). Don't pass an unexpanded ~ to file-reading tools — they don't expand it.
- Find the entry where key starts with
onebrain@ and scope == "project" and projectPath matches the current vault.
- If not found: 🟡
onebrain not found in installed_plugins.json — run /onboarding or /plugin to install.
- Before any path comparison, normalize
installPath separators with installPath.replaceAll('\\', '/') — Windows paths can mix backslashes and forward slashes, and substring matches against '/.claude/plugins/cache/' will silently fail otherwise.
- If the normalized
installPath contains /.claude/plugins/cache/: 🔴 Plugin loading from user cache — run /doctor --fix to pin to vault (Pass A in references/autofix-procedures.md handles the fix).
- If the normalized
installPath ends with .claude/plugins/onebrain: ✅ Plugin: vault-level.
AUTO-SUMMARY.md existence
- Check
.claude/plugins/onebrain/skills/startup/AUTO-SUMMARY.md exists. If missing: 🔴 AUTO-SUMMARY.md not found — auto session summary disabled; run /update to restore. (The CLI plugin-files check counts skills + agents + INSTRUCTIONS.md but does NOT validate every individual skill reference file.)
Stale PLUGIN-CHANGELOG.md at vault root (post-v3.0.2 migration cleanup)
- Check if
PLUGIN-CHANGELOG.md exists at vault root. If yes: 🟡 Stale PLUGIN-CHANGELOG.md at vault root — renamed to CHANGELOG.md in plugin v3.0.2. Run \rm PLUGIN-CHANGELOG.md` to clean up`. (vault sync currently doesn't delete files renamed-away from upstream — tracked for a future CLI fix.)
Scheduler health — the per-entry checks below run only when onebrain.yml has a schedule: block (they iterate its entries); the orphan-plist checks — Schedule drift → Orphan plist and Stale plist content-shape — instead glob installed com.onebrain.*.plist directly and run whenever any exist, regardless of the block. All plist paths are macOS/launchd-specific — on non-macOS hosts the glob is empty and these checks no-op.
- Errors: glob
[logs_folder]/scheduler/**/*.err.md from the last 7 days. If any → 🟡 report count + most recent 3 as wikilinks.
- Consecutive failures: for each
schedule: entry, count consecutive .err.md files from newest with no intervening success .md. If ≥ 3 → 🔴 CRITICAL — suggest onebrain schedule register --resume <skill>.
- Schedule drift — checked in both directions.
labelSafe = the entry's binary name (command mode) or its skill name with the leading / stripped (skill mode), with non-[a-zA-Z0-9-] chars replaced by -; each entry thus maps to com.onebrain.<labelSafe>.plist. (Same derivation as schedule-list's plist-existence fallback.)
- Missing plist — gated (needs the
schedule: block to iterate entries): for each entry, check its expected ~/Library/LaunchAgents/com.onebrain.<labelSafe>.plist exists. If missing → 🟡 onebrain schedule register to repair.
- Orphan plist — ungated (globs installed plists, not entries; mirrors Stale plist content-shape below): build the expected-name set
{ com.onebrain.<labelSafe>.plist } over every current schedule: entry — keyed by name, so entries that share a label (e.g. two /weekly crons) collapse to one — which is empty when there is no schedule: block. Then glob ~/Library/LaunchAgents/com.onebrain.*.plist; any installed plist whose filename is not in the expected set → 🟡 stale plist: {plist filename} — onebrain schedule register --remove. Runs even with an empty/absent schedule: block, since a plist left behind after the user clears their schedule is exactly the orphan to catch. When unsure whether an installed plist maps to a current entry, do not flag it (favor false-negative over false-positive). A plist can trip both this check and Stale plist content-shape; if it matches no current entry, emit only this orphan --remove line and suppress the content-shape line for that plist.
- One-shot reachability: for each
at: entry, verify timestamp hasn't already passed. If passed and plist still exists → 🟡 expired one-shot not cleaned up.
- Stale plist content-shape (pre-v2.3.3 / source-run artifacts; legacy Bun-era check — safe to remove after the 2026-06-30 Bun-CLI deprecation once no pre-v2.3.3 plists remain in the wild): for each installed
~/Library/LaunchAgents/com.onebrain.*.plist — scan these whenever they exist on disk, even if onebrain.yml has no schedule: block, since an orphan plist left behind after the user cleared their schedule is exactly the case to catch — read its <key>ProgramArguments</key> array. Flag 🟡 stale plist shape: {plist filename} — \onebrain schedule register` to regenerate (or `--remove` if it matches no current schedule: entry)` if either holds:
- (a) the array contains a
<string>--headless</string> element, or it contains a <string>--skill</string> element but no <string>run-skill</string> element anywhere in the array — both are pre-v2.3.3 shapes that silently fail on every fire. (v2.3.3 moved the scheduler entry point to the run-skill subcommand, which a valid recurring skill-mode plist carries as ProgramArguments[1]. Match on run-skill presence anywhere in the array — never by position relative to --skill/--vault — or every valid plist false-positives. onebrain update does not regenerate already-installed plists, so upgraders keep firing the broken shape.)
- (b)
ProgramArguments[0] (the first <string>, i.e. the executable path) ends in .ts or .js — happens when a dev registered from source (e.g. bun run src/index.ts …) instead of the compiled binary, so launchd has no interpreter on its restricted PATH.
- Scope: clauses (a)/(b) cover recurring argv-shaped plists. A one-shot (
at:) plist wraps its whole invocation in a single <string>/bin/sh -c '…'</string> element (args as --skill="…", run-skill inside the string), so the standalone-element match above neither false-positives on a valid one-shot nor catches a broken one — for a /bin/sh -c plist, inspect the embedded shell string instead: flag if it contains --headless, or --skill= with no run-skill, or invokes a .ts/.js entry point. (Low priority — one-shots postdate the v2.3.3 fix, so a broken one is unlikely.)
Pause: orphan pointer
- Read
[logs_folder]/pause/_active.md. If absent → skip. Parse slug. Glob [logs_folder]/pause/*-{slug}-pause-*.md. If empty match → ⚠️ Pause pointer references {slug} but no snapshot files exist. Fix: rm 07-logs/pause/_active.md (or create initial /pause).
Pause: missing pointer
- Glob
[logs_folder]/pause/*-pause-*.md. If empty → skip. If _active.md exists → skip. Otherwise → ⚠️ Pause files exist but no active pointer: + list slugs + counts + latest date. Fix: echo "{chosen-slug}" > 07-logs/pause/_active.md.
Pause: idle thread
- Read
_active.md. If absent → skip. Glob files for the slug. Get max date prefix. If (today - max_date).days > 14 → ⚠️ Pause thread {slug} idle for N days (last snapshot YYYY-MM-DD). Fix: /wrapup to consolidate, or /pause to refresh, or /resume to continue.
Memory health — run all checks from references/memory-health-checks.md. Findings go under the 🧠 Memory section.
Step 3: Merge + Report
Combine CLI doctor findings (Step 2a) and skill findings (Step 2b) into one unified report. Omit any section that has zero findings to keep the report focused — empty section headers add noise without information.
──────────────────────────────────────────────────────────────
🏥 OneBrain Doctor · YYYY-MM-DD
──────────────────────────────────────────────────────────────
⚙️ Config (from `onebrain doctor`) (omit under --vault flag, or when CLI ran and emitted 0 findings worth showing)
<one line per CLI check — use the JSON `message` and emoji from status>
📁 Vault (omit under --config flag, or when 0 vault findings)
<broken-links, orphan-notes, inbox-backlog, 07-logs structure, old checkpoints, log folder size, plugin install path, AUTO-SUMMARY.md, stale PLUGIN-CHANGELOG.md>
🧠 Memory (omit under --config flag, or when 0 memory findings)
<MEMORY.md size, stale memory/ files, MEMORY.md structure, memory-health checks>
📅 Scheduler (only when there are scheduler findings — orphan-plist / stale-shape findings can occur even with no `schedule:` block)
<errors, consecutive failures, missing plist, orphan / stale plist, expired one-shots>
⏸️ Pause (only if pause state has findings)
<orphan pointer, missing pointer, idle thread>
──────────────────────────────────────────────────────────────
{summary line}
Summary line:
- All green → ✅
Everything looks healthy. N checks · 0 issues.
- Otherwise →
🔴 N issues found (M critical, P warnings). Run /doctor --fix to repair safe issues. (use CLI's summary + skill issue count)
For each finding, prefer the CLI's message verbatim when it's from onebrain doctor (single source of truth for the built-in checks). For skill findings, render the action-oriented form (Fix: <command>).
Step 4: Auto-fix (--fix flag only)
Two-stream fix:
-
CLI fixes — already executed by onebrain doctor --fix --json in Step 2a. The top-level JSON fix[] array reports outcomes (fixed, partial, failed, manual — see the outcome table in Step 2a). Render under each affected check.
-
Skill fixes — Read references/autofix-procedures.md and run Pass A, B, C, D, E in order. Each pass confirms with the user before writing. After all passes, run onebrain search reindex as the Final step. Pass E de-links the 🔴 auto-memory mislinks detected in Step 2b (the 🟡 missing-note bucket is never auto-fixed).
The CLI fix recipes cover: settings-hooks, plugin-files, onebrain.yml-keys, folders, claude-settings, plugin-cache, vault-config-migration, and legacy-qmd-collection (migrates a top-level qmd_collection → search.collection). The skill fix passes cover: stale confidence-score updates, broken-wikilink fuzzy-match repair, auto-memory wikilink de-linking, MEMORY.md structure migration. Together: CLI handles config, skill handles content.
On Completion
-
Update onebrain.yml stats.last_doctor_run: YYYY-MM-DD. If --fix ran: also update stats.last_doctor_fix: YYYY-MM-DD.
-
Write doctor log entry. Follow ../_shared/audit-log-format.md (canonical frontmatter, append-per-day algorithm, run-section heading, failure mode) with:
- Filename:
YYYY-MM-DD-doctor.md — one file per day.
- Tags:
[audit-log, doctor].
- Skill:
/doctor
- Per-skill discriminator:
flags: [--vault, --config, --fix] (subset active this run; empty list = default — all checks).
Body template:
## Run HH:MM
- Flags: --vault, --config (or "default" when no flags)
### Findings
- 🔴/🟡/✅ <one line per finding>
### Fixes Applied
- <one line per fix; or "(none — diagnostic only)">
### Recommendations
- <one line per actionable recommendation>
-
Read and follow references/migration-safety-net.md at the end of every /doctor run.
Known Gotchas
-
Wikilinks in frontmatter YAML values are not navigable links. Fields like superseded_by: [[old-file]] contain wikilink syntax but Obsidian doesn't resolve them. The broken-link checker already skips fenced code blocks and blockquotes; also skip any [[...]] that appears on a line before the closing --- of the frontmatter block.
-
--fix is not transactional. If Pass C is interrupted (user says "stop", a file write fails), previously edited files are already changed but later ones are not. Report each fixed file immediately as it completes so the user has a clear record of what was and wasn't changed.
-
onebrain.yml with Windows line endings (CRLF). YAML values may have a trailing \r if edited on Windows. Strip trailing whitespace from any onebrain.yml-derived path string (e.g. value.replace(/\s+$/, '')) before passing it to file-existence checks, Glob, or Read — otherwise a folder named 00-inbox\r will silently fail to match the on-disk 00-inbox/.
-
CLI doctor JSON is the v3.x stable contract. If the JSON shape changes in a future v3.x release, the skill MUST update accordingly (CLI repo's CHANGELOG announces schema changes). If onebrain doctor --json fails to parse, treat it like CLI-not-installed (fall back to skill checks only) and surface the parse error.
-
onebrain doctor already handles the schema-policy checks. Don't duplicate them in skill body: onebrain.yml-keys, plugin-files integrity, settings-hooks (Stop checkpoint + PostToolUse search-reindex + Stop embed), and claude-settings (stale marketplace) are CLI-side. The skill's job is content-level checks (wikilinks, memory, pause, scheduler) the CLI doesn't know about.