| name | obsidian-setup |
| description | First-run configuration and upgrade for the Obsidian Brain plugin. Sets up vault path, creates folders, copies dashboard templates, configures hookify nudges, and writes config. Idempotent — safe to re-run to pick up new features without overwriting existing config or dashboards. Use when: (1) first time installing obsidian-brain, (2) changing vault path, (3) /obsidian-setup command, (4) upgrading to a new version. |
| metadata | {"version":"1.4.0"} |
Obsidian Brain Setup
Configure the obsidian-brain plugin for first use. This skill validates prerequisites, creates vault folders, installs dashboard templates, writes the config file, and verifies everything works.
Tools needed: Bash, Write, Read
Procedure
Follow these steps exactly. Do not skip steps or reorder them.
Step 1 — Check for existing installation
Argument handling: If the user ran /obsidian-setup --deps, skip the existing-config detection and jump directly to Step 8.7 (Performance Dependencies). --deps re-triggers the dependency prompt regardless of the optional_deps_prompted flag in config. After Step 8.7 completes, skip to Step 10 (success message).
Check for existing config:
cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
python3 -c '
import sys, os
import glob; sys.path.insert(0, max(glob.glob(os.path.expanduser("~/.claude/plugins/cache/*/obsidian-brain/*/hooks")), default="hooks"))
from obsidian_utils import load_config
c = load_config()
vp = c.get("vault_path", "")
if vp:
sess = c.get("sessions_folder", "claude-sessions")
ins = c.get("insights_folder", "claude-insights")
dash = c.get("dashboards_folder", "claude-dashboards")
chk = c.get("check_items_folder", "claude-check-items")
print(f"EXISTING")
print(f"VAULT={vp}")
print(f"SESS={sess}")
print(f"INS={ins}")
print(f"DASH={dash}")
print(f"CHK={chk}")
else:
print("NO_CONFIG")
'
If the output starts with EXISTING, parse each subsequent line as KEY=VALUE, splitting on the first =. Extract VAULT_PATH and present:
Existing obsidian-brain installation detected.
- Vault path:
<vault_path from config>
Would you like to:
- upgrade — add new features (dashboards, nudges) without touching existing config or dashboards
- reconfigure — start fresh (will overwrite config and dashboards)
- cancel — exit setup
If upgrade: store MODE=upgrade. Store VAULT_PATH from the existing config's vault_path field. Also extract sessions_folder (default claude-sessions), insights_folder (default claude-insights), dashboards_folder (default claude-dashboards), and check_items_folder (default claude-check-items). Skip to Step 5 (Create vault folders — mkdir -p is already safe). In upgrade mode:
- Step 5 (Create vault folders): runs normally (
mkdir -p is idempotent)
- Step 6 (Install dashboards): only write dashboard files that do NOT already exist (
test -f before each write)
- Step 7 (Write config): SKIP entirely — preserve existing config
- Step 8 (Verify vault access): runs normally
- Step 8.5 (Build vault index): runs normally (idempotent ensure_index call)
- Step 8.7 (Performance Dependencies): runs normally — has its own idempotency check via
optional_deps_prompted/optional_deps_declined config fields; users with declined deps are NOT re-prompted unless they explicitly run /obsidian-setup --deps
- Step 9 (Configure claudeception nudge): runs normally (has its own idempotency check)
- Step 10 (Print success message): show upgrade-specific message
If reconfigure: store MODE=reconfigure. Proceed to Step 2 (Ask for vault path) as normal — full setup flow.
If cancel: stop here.
If the output is NO_CONFIG: store MODE=fresh. Proceed to Step 2 (Ask for vault path) — first-time setup.
Step 1.5 — Permission pre-flight check
Before any out-of-workspace writes, test whether Claude Code can write to ~/.claude/:
echo "test" > ~/.claude/.obsidian-brain-canary 2>&1 && rm -f ~/.claude/.obsidian-brain-canary && echo "OK" || echo "FAIL"
If OK: proceed silently to Step 2.
If FAIL: present the following message using AskUserQuestion:
Heads up — setup needs write access outside this project directory.
Obsidian Brain writes config to ~/.claude/ and notes to your Obsidian vault. Your current Claude Code permissions block writes outside the working directory.
Choose how to fix this:
-
Switch permission mode (recommended) — Press Shift+Tab to switch to "accept edits" mode for this session. Or use /config to change permissions.defaultMode permanently. Then re-run /obsidian-setup.
-
Whitelist paths permanently — Add $HOME/.claude and your vault's parent directory to sandbox.filesystem.allowWrite in ~/.claude/settings.json. Use absolute paths — ~ is not expanded inside JSON string values:
{
"sandbox": {
"filesystem": {
"allowWrite": ["/Users/you/.claude", "/Users/you/Documents/vault-parent"]
}
}
}
Replace /Users/you with your actual home directory (run echo $HOME to find it). Then re-run /obsidian-setup.
-
I'll handle it myself — Continue setup and approve or fix writes as they come up.
Behavior per option:
- Option 1: Print the instruction, then stop. User changes mode and re-runs
/obsidian-setup.
- Option 2: Print the JSON snippet with absolute paths. In upgrade mode (
MODE=upgrade), substitute the known vault parent path from the existing config. In fresh mode, show only the $HOME/.claude entry with a note that the vault parent must be added after the user provides the vault path. Then stop. User edits settings and re-runs.
- Option 3: Continue with setup as normal. Writes may fail and the user deals with each one.
Step 2 — Ask for vault path
Ask the user:
What is the absolute path to your Obsidian vault? (e.g. /Users/you/obsidian/my-vault)
Store the response as VAULT_PATH. Strip any trailing slash.
Step 3 — Validate the vault path
Run:
test -d "$VAULT_PATH" && test -w "$VAULT_PATH" && echo "OK" || echo "FAIL"
If FAIL, tell the user the path does not exist or is not writable and ask them to correct it. Repeat until OK.
Step 4 — Check claude CLI availability
Run:
which claude && echo "OK" || echo "FAIL"
If FAIL, warn the user:
The claude CLI was not found on PATH. Hook-based AI summarization will not work until it is installed. You can continue setup, but session notes will be raw (unsummarized) until claude is available.
Continue regardless — this is a warning, not a blocker.
Step 5 — Create vault folders
First, test that the vault path is writable:
echo "test" > "$VAULT_PATH/.obsidian-brain-canary" 2>&1 && rm -f "$VAULT_PATH/.obsidian-brain-canary" && echo "OK" || echo "FAIL"
If FAIL, tell the user:
Cannot write to your vault at $VAULT_PATH. This is likely a sandbox restriction. Add your vault's parent directory to sandbox.filesystem.allowWrite in ~/.claude/settings.json, or switch to "accept edits" mode (Shift+Tab), then re-run /obsidian-setup.
Stop here if FAIL.
If OK, create the folders:
mkdir -p "$VAULT_PATH/claude-sessions" "$VAULT_PATH/claude-insights" "$VAULT_PATH/claude-dashboards"
Step 6 — Install dashboard templates
For each dashboard file below, check if it already exists before writing:
test -f "$VAULT_PATH/claude-dashboards/<filename>" && echo "EXISTS" || echo "MISSING"
If EXISTS and MODE=upgrade, skip this file — preserve user customizations.
Otherwise (MISSING, or MODE=fresh, or MODE=reconfigure), write the file using the Write tool.
Dashboard files to install:
File: $VAULT_PATH/claude-dashboards/sessions-overview.md
# Claude Sessions Overview
## Recent Sessions
\```dataview
TABLE date, project, git_branch, duration_minutes
FROM "claude-sessions"
WHERE type = "claude-session"
SORT date DESC
LIMIT 20
\```
## Recent Insights
\```dataview
TABLE date, project, tags
FROM "claude-insights"
WHERE type = "claude-insight"
SORT date DESC
LIMIT 10
\```
## Active Decisions
\```dataview
TABLE date, project
FROM "claude-insights"
WHERE type = "claude-decision" AND status = "active"
SORT date DESC
\```
File: $VAULT_PATH/claude-dashboards/project-index.md
# Project Index
## Sessions by Project
\```dataview
TABLE length(rows) AS "Sessions", min(rows.date) AS "First", max(rows.date) AS "Last"
FROM "claude-sessions"
WHERE type = "claude-session"
GROUP BY project
SORT length(rows) DESC
\```
## Error Fixes (troubleshooting library)
\```dataview
TABLE date, project
FROM "claude-insights"
WHERE type = "claude-error-fix"
SORT date DESC
\```
File: $VAULT_PATH/claude-dashboards/weekly-review.md
# This Week in Claude
\```dataview
TABLE date, project, type
FROM "claude-sessions" OR "claude-insights"
WHERE date >= date(today) - dur(7 days)
SORT date DESC
\```
File: $VAULT_PATH/claude-dashboards/learning-velocity.md
# Learning Velocity
## Topics by Frequency
\```dataviewjs
let pages = dv.pages('"claude-insights"')
.where(p => p.tags);
let topics = {};
for (let p of pages) {
let tags = p.tags || [];
if (!Array.isArray(tags)) tags = [tags];
for (let tag of tags) {
if (typeof tag === 'string' && tag.startsWith("claude/topic/")) {
let topic = tag.replace("claude/topic/", "");
topics[topic] = (topics[topic] || 0) + 1;
}
}
}
dv.table(
["Topic", "Notes"],
Object.entries(topics)
.sort((a, b) => b[1] - a[1])
.map(([topic, count]) => [topic, count])
);
\```
## Recent Retrospectives
\```dataview
TABLE date, project
FROM "claude-insights"
WHERE type = "claude-retro"
SORT date DESC
LIMIT 10
\```
## Error Patterns (Most Common)
\```dataviewjs
let pages = dv.pages('"claude-insights"')
.where(p => p.type === "claude-error-fix");
let topics = {};
for (let p of pages) {
let tags = p.tags || [];
if (!Array.isArray(tags)) tags = [tags];
for (let tag of tags) {
if (typeof tag === 'string' && tag.startsWith("claude/topic/")) {
let topic = tag.replace("claude/topic/", "");
topics[topic] = (topics[topic] || 0) + 1;
}
}
}
dv.table(
["Error Topic", "Occurrences"],
Object.entries(topics)
.sort((a, b) => b[1] - a[1])
.slice(0, 15)
.map(([topic, count]) => [topic, count])
);
\```
File: $VAULT_PATH/claude-dashboards/decision-timeline.md
# Decision Timeline
## Active Decisions
\```dataview
TABLE date, project
FROM "claude-insights"
WHERE type = "claude-decision" AND status = "active"
SORT date DESC
\```
## All Decisions (Chronological)
\```dataview
TABLE date, project, status
FROM "claude-insights"
WHERE type = "claude-decision"
SORT date DESC
\```
## Superseded Decisions
\```dataview
TABLE date, project
FROM "claude-insights"
WHERE type = "claude-decision" AND status = "superseded"
SORT date DESC
\```
## Decisions by Project
\```dataview
TABLE length(rows) AS "Decisions", min(rows.date) AS "First", max(rows.date) AS "Last"
FROM "claude-insights"
WHERE type = "claude-decision"
GROUP BY project
SORT length(rows) DESC
\```
File: $VAULT_PATH/claude-dashboards/open-items.md
# Open Items — All Projects
Cross-project view of all unchecked `- [ ]` items from session notes' `## Open Questions / Next Steps` sections, scoped to the last 90 days. Items older than 90 days fall off this view — use `/check-items` (unbounded) or `/vault-search` to find them.
\```dataviewjs
// Single-pass: scan all sessions in the last 90 days exactly once,
// build a master list, then render each section from in-memory data.
const cutoff90 = dv.date("today").minus(dv.duration("90 days"));
const cutoff30 = dv.date("today").minus(dv.duration("30 days"));
const cutoff7 = dv.date("today").minus(dv.duration("7 days"));
const pages = dv.pages('"claude-sessions"')
.where(p => p.type === "claude-session" && p.date && p.date >= cutoff90);
const allItems = [];
for (const p of pages) {
const content = await dv.io.load(p.file.path);
if (!content) continue;
// CRLF-tolerant: \r?\n in lookahead, split on /\r?\n/
const match = content.match(/## Open Questions[^\r\n]*\r?\n([\s\S]*?)(?=\r?\n## |\r?\n# |$)/);
if (!match) continue;
const lines = match[1].split(/\r?\n/);
for (const line of lines) {
const m = line.match(/^- \[ \] (.+?)\s*$/);
if (m) {
allItems.push({
project: p.project || "unknown",
item: m[1],
date: p.date,
file: p.file.link
});
}
}
}
// ----- By Project -----
dv.header(2, "By Project");
if (allItems.length === 0) {
dv.paragraph("No open items in the last 90 days.");
} else {
const byProject = {};
for (const i of allItems) {
if (!byProject[i.project]) byProject[i.project] = [];
byProject[i.project].push(i);
}
const projectNames = Object.keys(byProject).sort();
for (const project of projectNames) {
const items = byProject[project];
dv.header(3, project + " (" + items.length + ")");
// Render as table to preserve clickable file link
dv.table(
["Item", "Source"],
items.map(i => [i.item, i.file])
);
}
}
// ----- Recent (last 7 days) -----
dv.header(2, "Recent (last 7 days)");
const recent = allItems.filter(i => i.date >= cutoff7);
if (recent.length === 0) {
dv.paragraph("No open items from sessions in the last 7 days.");
} else {
dv.table(
["Project", "Item", "Source"],
recent.map(i => [i.project, i.item, i.file])
);
}
// ----- Items from sessions 30-90 days ago -----
dv.header(2, "Items from sessions 30-90 days ago");
dv.paragraph("These are unchecked items captured in session notes that are 30-90 days old. The same item may also appear in a more recent session — in that case it will also show in the \"Recent\" section above. Filter is by source session date, not by item-tracking duration.");
const stale = allItems
.filter(i => i.date < cutoff30)
.sort((a, b) => a.date - b.date);
if (stale.length === 0) {
dv.paragraph("No items from sessions 30-90 days ago.");
} else {
dv.table(
["Project", "Item", "From", "Source"],
stale.map(i => [i.project, i.item, i.date.toFormat("yyyy-MM-dd"), i.file])
);
}
// ----- Stats -----
dv.header(2, "Stats");
const statsByProject = {};
let oldestDate = null;
for (const i of allItems) {
statsByProject[i.project] = (statsByProject[i.project] || 0) + 1;
if (!oldestDate || i.date < oldestDate) oldestDate = i.date;
}
dv.paragraph("**Total open items (last 90 days):** " + allItems.length);
dv.paragraph("**Projects with open items:** " + Object.keys(statsByProject).length);
if (oldestDate) {
dv.paragraph("**Oldest open item from:** " + oldestDate.toFormat("yyyy-MM-dd"));
}
if (Object.keys(statsByProject).length > 0) {
dv.table(
["Project", "Open Items"],
Object.entries(statsByProject).sort((a, b) => b[1] - a[1])
);
}
\```
Important: The backslash before the triple backticks above is an escape for this document only. When writing the actual files, use plain triple backticks (no backslash).
Step 7 — Write config file
If MODE=upgrade: Skip this step — preserve existing config.
If MODE=fresh or MODE=reconfigure:
Write ~/.claude/obsidian-brain-config.json with this exact structure:
{
"vault_path": "<VAULT_PATH value from Step 2>",
"sessions_folder": "claude-sessions",
"insights_folder": "claude-insights",
"dashboards_folder": "claude-dashboards",
"check_items_folder": "claude-check-items",
"min_messages": 3,
"min_duration_minutes": 2,
"summary_model": "haiku",
"auto_log_enabled": true,
"snapshot_on_compact": true,
"snapshot_on_clear": true
}
Replace <VAULT_PATH value from Step 2> with the actual vault path. Ensure the file is valid JSON.
First run mkdir -p ~/.claude to ensure the directory exists.
After writing the config file, restrict permissions so only the current user can read it:
chmod 600 ~/.claude/obsidian-brain-config.json
After writing the config, inspect the final snapshot_on_clear and snapshot_on_compact values. If either is False (for instance, when migrating from an older config), warn the user:
⚠ snapshot_on_clear is False — /clear will not preserve pre-clear context.
Recommended: True. (This is on by default; only change if you have a specific reason.)
Both flags ship true by default, so the warning only fires when a prior config, manual edit, or migration set them false. Ask the user whether to flip them back to true via /vault-config before continuing.
Step 8 — Verify vault access
Run:
TESTFILE="$VAULT_PATH/claude-sessions/.obsidian-brain-test-$$"
echo "test" > "$TESTFILE" && test -f "$TESTFILE" && rm "$TESTFILE" && echo "OK" || echo "FAIL"
If FAIL, warn that vault writes are not working and ask the user to check permissions.
Step 8.5 — Build vault index
Build (or rebuild) the SQLite FTS5 index for fast vault search and context-driven insight loading:
python3 -c '
import sys, os, json, glob
sys.path.insert(0, max(glob.glob(os.path.expanduser("~/.claude/plugins/cache/*/obsidian-brain/*/hooks")), default="hooks"))
from vault_index import rebuild_index
counts = rebuild_index(sys.argv[1], [sys.argv[2], sys.argv[3]])
print(json.dumps(counts))
' "$VAULT_PATH" "$SESSIONS_FOLDER" "$INSIGHTS_FOLDER"
Parse the JSON output. If successful, store N = counts["inserted"] for the success message.
If the command fails (non-zero exit), warn but do not block setup:
⚠️ Could not build vault index. Run /vault-reindex manually after setup.
Step 8.7 — Performance Dependencies (optional)
Phase 2 theme clustering can use numpy (vectorized TF-IDF) and scipy (agglomerative batch clustering) for ~4-5x speedups on large vaults. They are strictly optional — every feature works on pure-Python stdlib fallbacks.
Idempotent detection. Load the current config + installed status:
cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
python3 -c '
import sys, os, json
import glob; sys.path.insert(0, max(glob.glob(os.path.expanduser("~/.claude/plugins/cache/*/obsidian-brain/*/hooks")), default="hooks"))
from obsidian_utils import check_optional_deps, load_config
cfg = load_config()
status = check_optional_deps()
print("NUMPY=" + ("1" if status["numpy"] else "0"))
print("SCIPY=" + ("1" if status["scipy"] else "0"))
print("PROMPTED=" + ("1" if cfg.get("optional_deps_prompted") else "0"))
print("DECLINED=" + ",".join(cfg.get("optional_deps_declined", [])))
'
Parse each line as KEY=VALUE.
Decision tree:
- If
NUMPY=1 and SCIPY=1 → both installed; skip the prompt entirely. Log one line [setup] numpy + scipy available — theme clustering will use fast paths. and continue to Step 9.
- If
PROMPTED=1 AND every missing package appears in DECLINED AND this run was NOT invoked with --deps → user already declined; skip the prompt silently. Continue to Step 9.
- Otherwise → show the prompt below.
Prompt the user using AskUserQuestion:
Optional performance dependencies for Phase 2 theme clustering:
numpy: [installed / not installed]
scipy: [installed / not installed]
These accelerate batch clustering and TF-IDF math (numpy ~5x, scipy ~4x). Without them, everything still works via pure-Python stdlib fallbacks — daily use is unaffected; only bulk /consolidate runs are slower at very large vault sizes.
- Install missing packages (runs
python3 -m pip install --user <missing>)
- Skip — remember my choice (won't ask again; re-trigger anytime with
/obsidian-setup --deps)
- Not now — ask again next time
Apply the choice:
- Install: run
python3 -m pip install --user for every missing package. Report success/failure for each. After install, update config:
cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
python3 -c '
import sys, os, json, tempfile
import glob; sys.path.insert(0, max(glob.glob(os.path.expanduser("~/.claude/plugins/cache/*/obsidian-brain/*/hooks")), default="hooks"))
from pathlib import Path
cfg_path = Path.home() / ".claude" / "obsidian-brain-config.json"
try:
cfg = json.loads(cfg_path.read_text())
except FileNotFoundError:
cfg = {}
cfg["optional_deps_prompted"] = True
declined = set(cfg.get("optional_deps_declined", []))
for pkg in ("numpy", "scipy"):
try:
__import__(pkg)
declined.discard(pkg)
except ImportError:
pass
cfg["optional_deps_declined"] = sorted(declined)
fd, tmp_path = tempfile.mkstemp(prefix=".obsidian-brain-config-", suffix=".json.tmp", dir=os.path.dirname(cfg_path))
try:
with os.fdopen(fd, "w") as f:
json.dump(cfg, f, indent=2)
f.flush()
os.fsync(f.fileno())
os.chmod(tmp_path, 0o600)
os.replace(tmp_path, cfg_path)
except Exception:
try:
os.unlink(tmp_path)
except OSError:
pass
raise
print("OK")
'
If pip itself is missing or the install fails (restricted environments, e.g. Homebrew-managed Python), print the failure and continue — all features still work.
- Skip: write both flags with every missing package added to
optional_deps_declined:
cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
python3 - <<'PY'
import json, os, tempfile
from pathlib import Path
cfg_path = Path.home() / ".claude" / "obsidian-brain-config.json"
try:
cfg = json.loads(cfg_path.read_text())
except FileNotFoundError:
cfg = {}
cfg["optional_deps_prompted"] = True
missing = []
for pkg in ("numpy", "scipy"):
try:
__import__(pkg)
except ImportError:
missing.append(pkg)
declined = sorted(set(cfg.get("optional_deps_declined", [])) | set(missing))
cfg["optional_deps_declined"] = declined
fd, tmp_path = tempfile.mkstemp(prefix=".obsidian-brain-config-", suffix=".json.tmp", dir=os.path.dirname(cfg_path))
try:
with os.fdopen(fd, "w") as f:
json.dump(cfg, f, indent=2)
f.flush()
os.fsync(f.fileno())
os.chmod(tmp_path, 0o600)
os.replace(tmp_path, cfg_path)
except Exception:
try:
os.unlink(tmp_path)
except OSError:
pass
raise
print("OK")
PY
- Not now: write neither flag. The next
/obsidian-setup run will re-prompt.
Step 9 — Configure claudeception nudge (idempotent)
Check if the claudeception-to-compress nudge is already configured globally (in ~/.claude/, not the project .claude/):
test -f ~/.claude/hookify.claudeception-compress-nudge.local.md && echo "EXISTS" || echo "MISSING"
If EXISTS, skip this step — the nudge is already configured.
If MISSING, write the hookify rule file directly to ~/.claude/ using the Write tool:
File: ~/.claude/hookify.claudeception-compress-nudge.local.md
---
name: claudeception-compress-nudge
enabled: true
event: stop
pattern: Result:\s*PASS|\.claude/skills/[^/]+/SKILL\.md|created skill|skill file written|extracted knowledge
action: warn
---
💡 **Claudeception extracted knowledge from this session.** Run `/compress` to save it to your Obsidian vault.
Important: This rule MUST be in ~/.claude/ (global), not the project's .claude/ directory. The nudge should trigger in any project where claudeception runs, not just obsidian-brain.
This is a soft nudge — a non-blocking suggestion, not automatic execution.
Step 10 — Print success message
If MODE=upgrade:
Obsidian Brain upgraded!
- Vault path:
<VAULT_PATH> (unchanged)
- Config: preserved (unchanged)
- New dashboards: installed (existing dashboards preserved)
- Claudeception nudge: configured
- Vault index: N notes indexed (run
/vault-reindex to rebuild) — or omit this line if Step 8.5 failed
Re-run /obsidian-setup anytime to pick up new features.
If MODE=fresh or MODE=reconfigure:
Obsidian Brain setup complete!
- Vault path:
<VAULT_PATH>
- Config written to:
~/.claude/obsidian-brain-config.json
- Folders created:
claude-sessions/, claude-insights/, claude-dashboards/
- Dashboards installed:
sessions-overview.md, project-index.md, weekly-review.md, learning-velocity.md, decision-timeline.md, open-items.md
- Claudeception nudge: configured (run
/compress reminder after knowledge extraction)
- Vault index: N notes indexed (run
/vault-reindex to rebuild) — or omit this line if Step 8.5 failed
Next step — install the Dataview plugin in Obsidian:
- Open Obsidian Settings > Community Plugins > Browse
- Search for "Dataview" and install it
- Enable the plugin, then go to Dataview settings and turn on:
- Enable JavaScript Queries
- Enable Inline Queries
- The dashboards in
claude-dashboards/ will start rendering automatically
Hooks are already registered via hooks.json — session logging will begin on your next Claude Code session.