
Vault Setup
- 59 installs
- 6 repo stars
- Updated July 22, 2026
- julianobarbosa/claude-code-skills
Interactive Obsidian vault configurator. Use when setting up Obsidian vault, creating second brain, initializing knowledge base, vault bootstrap, configuring setup.
About
Interactive Obsidian vault configurator.. Use for Obsidian vault setup, second brain creation, knowledge base init, vault bootstrap, config setup.
- intermediate skill
- core: security
Vault Setup by the numbers
- 59 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,237 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/julianobarbosa/claude-code-skills --skill vault-setupAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 59 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 22, 2026 |
| Repository | julianobarbosa/claude-code-skills ↗ |
What it does
Interactive Obsidian vault configurator. Use when setting up Obsidian vault, creating second brain, initializing knowledge base, vault bootstrap, configuring setup.
Files
vault-setup-skill
Interactive vault configurator — asks one free-text question, infers your role and folders, builds a personalized Obsidian vault with CLAUDE.md and companion skill links.
Directory Structure
vault-setup-skill/
├── SKILL.md # This file (routing + quick ref)
├── RoleTemplates.md # Keyword-to-folder mapping table
├── PluginRecommendations.md # Obsidian plugins by role type
├── Workflows/
│ ├── Setup.md # Main 6-step setup flow
│ ├── ImportFiles.md # File import guidance
│ └── Verify.md # Post-setup verification
├── Tools/
│ ├── VaultBuilder.py # CLI: create, verify, inject-global
│ └── VaultBuilder.help.md # Tool documentation
└── scripts/
└── process_docs_to_obsidian.py # Bulk file import to inbox/Workflow Routing
| Intent | Workflow | Tool |
|---|---|---|
| Set up a new vault | Workflows/Setup.md | Tools/VaultBuilder.py create |
| Import existing files | Workflows/ImportFiles.md | scripts/process_docs_to_obsidian.py |
| Verify vault setup | Workflows/Verify.md | Tools/VaultBuilder.py verify |
| Add vault to global context | Workflows/Setup.md Step 5 | Tools/VaultBuilder.py inject-global |
| Choose Obsidian plugins | PluginRecommendations.md | — |
| Understand folder mapping | RoleTemplates.md | — |
Examples
"Set up my Obsidian vault" → Workflows/Setup.md
"I want to create a second brain" → Workflows/Setup.md
"Import my documents into the vault" → Workflows/ImportFiles.md
"Verify my vault is set up correctly" → Workflows/Verify.md
"What plugins should I install?" → PluginRecommendations.md
"Add this vault to my global config" → Workflows/Setup.md Step 5Quick Reference
Base folders (always created): inbox/, daily/, projects/, archive/
Additional folders detected from keywords — see RoleTemplates.md for full mapping.
Companion skills (linked, not created): daily-skill, tldr-skill, obsidian-master-skill
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
click not found when running VaultBuilder.py | Missing dependency | pip install click |
| Vault folders created but Obsidian doesn't see them | Obsidian not pointed at vault root | Open Obsidian → "Open folder as vault" → select the vault directory |
inject-global says "Already configured" but Claude doesn't see context | Vault path in CLAUDE.md doesn't match exactly | Check ~/.claude/CLAUDE.md for the vault path — it must be the resolved absolute path |
Symlink shows [BROKEN] in verify | Target skill directory was moved or deleted | Re-create the symlink: ln -sf /path/to/skill ~/.claude/skills/skill-name |
process_docs_to_obsidian.py shows encoding warnings | Source files contain non-UTF-8 characters | Review warned files manually; convert source encoding with iconv if needed |
Edge Cases
- Empty keywords: If
--role-keywordsis empty, only base folders are created (inbox, daily, projects, archive) - Vault path doesn't exist: VaultBuilder creates it automatically via
mkdir -p - Running create twice: Safe — folders use
exist_ok=True, CLAUDE.md is overwritten (back up first if customized)
---
Gotchas
- `inject-global` mutates `~/.claude/CLAUDE.md` with absolute path: If you later move the vault, the global config still points to the old path and Claude silently loads nothing. Re-run
inject-globalafter any vault relocation. - Re-running `create` overwrites a customized CLAUDE.md: Folder creation uses
exist_ok=True(safe) but CLAUDE.md is unconditionally rewritten. Back up first if you've hand-edited vault-level instructions. - `process_docs_to_obsidian.py` silently skips non-UTF-8 files: Latin-1 or Windows-1252 source files emit a warning and are not imported. The summary report doesn't flag them as failures — grep stderr for "encoding" before trusting the count.
- Symlinks to companion skills break on PAI directory restructure: Verify shows
[BROKEN]only if the symlink target is missing entirely; a renamed-but-existing target appears valid yet routes to the wrong skill. Re-run verify after any PAI skill reorganization. - Empty `--role-keywords` is not an error: The vault gets only base folders (inbox/daily/projects/archive) with no warning. If you expected role-specific folders, check the inferred keywords in the setup transcript.
- Obsidian doesn't see the vault until "Open folder as vault": Creating the directory tree is not enough — Obsidian must be pointed at the vault root explicitly. New users often think the script failed because Obsidian shows an empty workspace.
Plugin Recommendations
Shown as text recommendations during vault setup. Obsidian plugins are NOT auto-installed — the user installs them manually through Obsidian Settings → Community Plugins.
Core Plugins (All Roles)
| Plugin | Why |
|---|---|
| Dataview | Query notes like a database — essential for dashboards |
| Templater | Dynamic templates with date/folder logic |
| Calendar | Visual daily note navigation |
| Tasks | Track to-dos across all notes with due dates |
By Role Type
Engineering / DevOps
| Plugin | Why |
|---|---|
| Kanban | Visual project boards per client or sprint |
| Git | Version control your vault (auto-backup) |
| Periodic Notes | Weekly/monthly reviews alongside daily notes |
Business / Management
| Plugin | Why |
|---|---|
| Kanban | Track projects and decisions visually |
| Periodic Notes | Weekly reviews, monthly retros |
| Projects | Advanced project tracking with custom views |
Creative / Content
| Plugin | Why |
|---|---|
| Excalidraw | Visual brainstorming and diagrams |
| Outliner | Better list editing for content outlines |
| Slides | Turn notes into presentations |
Research / Academic
| Plugin | Why |
|---|---|
| Citations | BibTeX integration for academic references |
| Zotero Integration | Sync research library with vault |
| Reading Highlights | Import highlights from Kindle, Readwise |
Student
| Plugin | Why |
|---|---|
| Spaced Repetition | Flashcards from your notes |
| Outliner | Better list/outline manipulation |
Installation
1. Open Obsidian → Settings → Community Plugins 2. Disable Restricted Mode (if first time) 3. Browse → Search for plugin name → Install → Enable
Role Templates — Keyword-to-Folder Mapping
Used by Workflows/Setup.md Step 2 to infer vault folders from the user's free-text description. Base folders (inbox/, daily/, projects/, archive/) are always created.
| Keywords Detected | Folder | Purpose |
|---|---|---|
| pipeline, infrastructure, devops, cloud, deploy, terraform, kubernetes | runbooks/ | Operational procedures and troubleshooting |
| client, customer, account, engagement | clients/ | Per-client or per-team context |
| research, evaluate, architecture, tool, compare | research/ | Investigations, tool evals, ADRs |
| content, blog, video, podcast, newsletter | content/ | Content production pipeline |
| meeting, standup, retro, 1-on-1 | meetings/ | Meeting notes and action items |
| personal, life, health, family, finance | personal/ | Personal life tracking |
| notes, class, lecture, course, study | notes/ | Study materials and course notes |
| decision, adr, tradeoff | decisions/ | Architecture/business decision records |
| people, team, reports, manager | people/ | People tracking, org context |
| operations, sop, procedure, runbook | operations/ | Standard operating procedures |
| sales, deal, pipeline, crm | sales/ | Sales pipeline and deal tracking |
Example CLAUDE.md 'How I Work' Bullets by Domain
DevOps / Cloud Engineer:
- I automate everything — if it runs more than once, it gets a pipeline
- I manage many Azure subscriptions and need full tenant visibility
- I prefer concise, actionable documentation over verbose guides
- When I capture something, sort it fast — inbox should stay empty
Business / Consultant:
- I juggle multiple client engagements simultaneously
- I need fast access to client history and decision context
- I document decisions with rationale so I can recall why we chose X
Developer:
- I think in terms of systems and architecture
- I research tools before adopting — research/ is my evaluation zone
- I track client work separately from personal projects
Content Creator:
- I produce content across multiple formats (blog, video, podcast)
- Ideas go through a pipeline: capture → develop → produce → publish
- I need my AI to match my voice after reading recent drafts
Student:
- I organize by course and topic, not by date
- I need synthesis across lectures, readings, and my own notes
- Spaced repetition is part of my workflow
Context Rules by Detected Folder Set
- runbooks/: When I mention a procedure → check runbooks/ first
- clients/: When I mention a client → look in clients/
- research/: When I mention a tool or evaluation → check research/
- content/: When I mention content or a draft → check content/
- meetings/: When I mention a meeting → check meetings/
- personal/: Personal items stay in personal/ — don't mix with work
- decisions/: When I mention a decision → check decisions/ first
- people/: When I mention someone on my team → check people/
Companion Skill Recommendations by Domain
| Domain Keywords | Suggested Skills |
|---|---|
| devops, cloud, infrastructure | azure-devops-skill, container-security-skill, managing-infra-skill |
| content, blog, video | file-intel-skill for bulk processing |
| research, evaluate | file-intel-skill for document analysis |
| All domains | daily-skill, tldr-skill, obsidian-master-skill |
#!/usr/bin/env python3
"""Import files into an Obsidian vault's inbox folder.
Usage:
python process_docs_to_obsidian.py SOURCE_DIR VAULT_DIR
Copies supported files (.md, .txt, .pdf) from SOURCE_DIR to VAULT_DIR/inbox/,
adding basic frontmatter to .md files. Skips files that already exist.
"""
import argparse
import shutil
import sys
from datetime import datetime
from pathlib import Path
SUPPORTED_EXTENSIONS = {".md", ".txt", ".pdf", ".docx"}
def add_frontmatter(content: str, source_path: Path) -> str:
"""Add basic frontmatter to markdown content if not already present."""
if content.startswith("---"):
return content # Already has frontmatter
now = datetime.now().strftime("%Y-%m-%dT%H:%M")
frontmatter = f"""---
created: {now}
imported_from: {source_path}
status: inbox
tags:
- imported
---
"""
return frontmatter + content
def import_files(source_dir: Path, vault_dir: Path) -> dict:
"""Import files from source to vault inbox."""
inbox = vault_dir / "inbox"
inbox.mkdir(parents=True, exist_ok=True)
stats = {"imported": 0, "skipped": 0, "errors": 0, "files": []}
for file_path in sorted(source_dir.iterdir()):
if not file_path.is_file():
continue
if file_path.suffix.lower() not in SUPPORTED_EXTENSIONS:
continue
if file_path.name.startswith("."):
continue
target = inbox / file_path.name
# Skip if already exists
if target.exists():
stats["skipped"] += 1
print(f" [skip] {file_path.name} (already exists)")
continue
try:
if file_path.suffix.lower() == ".md":
# Add frontmatter to markdown files
raw_bytes = file_path.read_bytes()
try:
content = raw_bytes.decode("utf-8")
except UnicodeDecodeError:
content = raw_bytes.decode("utf-8", errors="replace")
print(f" [warn] {file_path.name}: encoding issues detected, some characters replaced")
content = add_frontmatter(content, file_path)
target.write_text(content, encoding="utf-8")
else:
# Binary copy for PDF, DOCX, etc.
shutil.copy2(file_path, target)
stats["imported"] += 1
stats["files"].append(file_path.name)
print(f" [ok] {file_path.name}")
except Exception as e:
stats["errors"] += 1
print(f" [error] {file_path.name}: {e}")
return stats
def main():
parser = argparse.ArgumentParser(
description="Import files into an Obsidian vault's inbox folder."
)
parser.add_argument("source", type=Path, help="Source directory containing files to import")
parser.add_argument("vault", type=Path, help="Target Obsidian vault directory")
args = parser.parse_args()
if not args.source.is_dir():
print(f"Error: Source directory not found: {args.source}", file=sys.stderr)
sys.exit(1)
if not args.vault.is_dir():
print(f"Error: Vault directory not found: {args.vault}", file=sys.stderr)
sys.exit(1)
print(f"Importing from {args.source} → {args.vault}/inbox/\n")
stats = import_files(args.source, args.vault)
print(f"\nDone: {stats['imported']} imported, {stats['skipped']} skipped, {stats['errors']} errors")
if stats["imported"] > 0:
print(f'\nNext step: tell Claude "Sort everything in inbox/"')
if __name__ == "__main__":
main()
#!/usr/bin/env bash
# Skill Quality Scorer — outputs a single integer score (0-100)
# Evaluates vault-setup-skill, daily-skill, tldr-skill across 5 dimensions.
# DO NOT MODIFY after initial creation — only skill files may change.
set -uo pipefail
SCRIPTS_DIR="$(cd "$(dirname "$0")" && pwd)"
VAULT_SETUP="$(cd "$SCRIPTS_DIR/.." && pwd)"
SKILLS_ROOT="$(cd "$VAULT_SETUP/.." && pwd)"
DAILY="$SKILLS_ROOT/daily-skill"
TLDR="$SKILLS_ROOT/tldr-skill"
SCORE=0
DETAILS=""
add() {
local pts=$1 max=$2 desc=$3
SCORE=$((SCORE + pts))
DETAILS="${DETAILS}\n [${pts}/${max}] ${desc}"
}
# ═══════════════════════════════════════════
# COMPLETENESS (30 pts)
# ═══════════════════════════════════════════
# Each skill SKILL.md has YAML frontmatter with name + description (3 x 2pts = 6)
for skill_dir in "$VAULT_SETUP" "$DAILY" "$TLDR"; do
skill_file="$skill_dir/SKILL.md"
if [ -f "$skill_file" ]; then
if head -5 "$skill_file" | grep -q "^name:"; then
if head -10 "$skill_file" | grep -q "^description:"; then
add 2 2 "$(basename "$skill_dir")/SKILL.md has name+description frontmatter"
else
add 1 2 "$(basename "$skill_dir")/SKILL.md has name but missing description"
fi
else
add 0 2 "$(basename "$skill_dir")/SKILL.md missing frontmatter"
fi
else
add 0 2 "$(basename "$skill_dir")/SKILL.md not found"
fi
done
# Each skill has a Troubleshooting/Error section (3 x 3pts = 9)
for skill_dir in "$VAULT_SETUP" "$DAILY" "$TLDR"; do
skill_file="$skill_dir/SKILL.md"
if [ -f "$skill_file" ] && grep -qi "troubleshoot\|error handling\|edge.case\|what.if" "$skill_file"; then
add 3 3 "$(basename "$skill_dir") has troubleshooting/error section"
else
add 0 3 "$(basename "$skill_dir") MISSING troubleshooting/error section"
fi
done
# vault-setup SKILL.md has examples section (2pts)
if grep -q "## Examples" "$VAULT_SETUP/SKILL.md" 2>/dev/null; then
add 2 2 "vault-setup-skill has Examples section"
else
add 0 2 "vault-setup-skill MISSING Examples section"
fi
# Each workflow file has at least one code example (3 x 2pts = 6)
for wf in "$VAULT_SETUP/Workflows/Setup.md" "$VAULT_SETUP/Workflows/Verify.md" "$VAULT_SETUP/Workflows/ImportFiles.md"; do
if [ -f "$wf" ] && grep -q '```' "$wf"; then
add 2 2 "$(basename "$wf") has code examples"
else
add 0 2 "$(basename "$wf") MISSING code examples"
fi
done
# daily-skill references/templates.md path is explicit (2pts)
if [ -f "$DAILY/SKILL.md" ] && grep -qi "references/templates\.md" "$DAILY/SKILL.md"; then
# Check if the path is explicit (relative to skill root or absolute)
if grep -qi "skill.root\|skill.dir\|relative to\|located at\|skill folder" "$DAILY/SKILL.md"; then
add 2 2 "daily-skill has explicit template path reference"
else
add 1 2 "daily-skill mentions templates.md but path not explicit"
fi
else
add 0 2 "daily-skill MISSING template path reference"
fi
# VaultBuilder.help.md covers all 3 commands (3pts)
help_file="$VAULT_SETUP/Tools/VaultBuilder.help.md"
if [ -f "$help_file" ]; then
cmds_found=0
grep -qi "create" "$help_file" && cmds_found=$((cmds_found + 1))
grep -qi "verify" "$help_file" && cmds_found=$((cmds_found + 1))
grep -qi "inject.global" "$help_file" && cmds_found=$((cmds_found + 1))
add $cmds_found 3 "VaultBuilder.help.md covers $cmds_found/3 commands"
else
add 0 3 "VaultBuilder.help.md not found"
fi
# ═══════════════════════════════════════════
# CORRECTNESS (25 pts)
# ═══════════════════════════════════════════
# No hardcoded user-specific vault paths in shared skills (5pts, deduct per occurrence)
hardcoded_count=0
for skill_dir in "$VAULT_SETUP" "$DAILY" "$TLDR"; do
if [ -d "$skill_dir" ]; then
count=$(grep -r "\.obsidian/vaults/\.obsidian-barbosa" "$skill_dir" --include="*.md" --include="*.py" 2>/dev/null | wc -l | tr -d ' ')
hardcoded_count=$((hardcoded_count + count))
fi
done
if [ "$hardcoded_count" -eq 0 ]; then
add 5 5 "No hardcoded user-specific vault paths"
elif [ "$hardcoded_count" -le 2 ]; then
add 2 5 "$hardcoded_count hardcoded vault path(s) found"
else
add 0 5 "$hardcoded_count hardcoded vault paths found"
fi
# VaultBuilder.py: open_obsidian parameter does not shadow function (5pts)
vb="$VAULT_SETUP/Tools/VaultBuilder.py"
if [ -f "$vb" ]; then
# Check if the create function's parameter name matches the function name
if grep -q "def create.*open_obsidian)" "$vb" && grep -q "def open_obsidian" "$vb"; then
add 0 5 "VaultBuilder.py: open_obsidian parameter SHADOWS the function"
else
add 5 5 "VaultBuilder.py: no parameter/function name shadowing"
fi
else
add 0 5 "VaultBuilder.py not found"
fi
# Python files pass syntax check (5pts)
py_syntax_ok=0
py_total=0
for pyfile in "$VAULT_SETUP/Tools/VaultBuilder.py" "$VAULT_SETUP/scripts/process_docs_to_obsidian.py"; do
if [ -f "$pyfile" ]; then
py_total=$((py_total + 1))
if python3 -c "compile(open('$pyfile').read(), '$pyfile', 'exec')" 2>/dev/null; then
py_syntax_ok=$((py_syntax_ok + 1))
fi
fi
done
if [ "$py_total" -gt 0 ]; then
pts=$(( (py_syntax_ok * 5) / py_total ))
add $pts 5 "Python syntax: $py_syntax_ok/$py_total files pass"
else
add 0 5 "No Python files found"
fi
# SKILL.md name field matches directory name (3 x ~2pts ≈ 5pts, round to 5)
name_match=0
for skill_dir in "$VAULT_SETUP" "$DAILY" "$TLDR"; do
skill_file="$skill_dir/SKILL.md"
if [ -f "$skill_file" ]; then
dir_name=$(basename "$skill_dir" | sed 's/-skill$//')
skill_name=$(grep "^name:" "$skill_file" 2>/dev/null | head -1 | sed 's/^name:[[:space:]]*//')
if [ -n "$skill_name" ]; then
# Check if name field roughly matches directory
if echo "$dir_name" | grep -qi "$skill_name" || echo "$skill_name" | grep -qi "$dir_name"; then
name_match=$((name_match + 1))
fi
fi
fi
done
case $name_match in
3) add 5 5 "All skill names match directory names" ;;
2) add 3 5 "$name_match/3 skill names match directory names" ;;
1) add 2 5 "$name_match/3 skill names match directory names" ;;
*) add 0 5 "No skill names match directory names" ;;
esac
# ═══════════════════════════════════════════
# ERROR RECOVERY (20 pts)
# ═══════════════════════════════════════════
# Each skill has error/edge-case handling section (3 x 4pts = 12)
for skill_dir in "$VAULT_SETUP" "$DAILY" "$TLDR"; do
found=0
# Check all markdown files in the skill
while IFS= read -r -d '' mdfile; do
if grep -qi "error\|edge.case\|what.if.*fail\|troubleshoot\|fallback\|graceful" "$mdfile" 2>/dev/null; then
found=1
break
fi
done < <(find "$skill_dir" -name "*.md" -print0 2>/dev/null)
if [ "$found" -eq 1 ]; then
add 4 4 "$(basename "$skill_dir") has error/edge-case content"
else
add 0 4 "$(basename "$skill_dir") MISSING error/edge-case content"
fi
done
# VaultBuilder.py has meaningful error messages (4pts)
if [ -f "$vb" ]; then
err_count=$(grep -c "print.*Error\|click.echo.*error\|sys.exit\|raise\|except" "$vb" 2>/dev/null || echo 0)
if [ "$err_count" -ge 4 ]; then
add 4 4 "VaultBuilder.py has $err_count error handling points"
elif [ "$err_count" -ge 2 ]; then
add 2 4 "VaultBuilder.py has $err_count error handling points (needs more)"
else
add 0 4 "VaultBuilder.py has insufficient error handling"
fi
else
add 0 4 "VaultBuilder.py not found"
fi
# process_docs_to_obsidian.py warns on encoding issues (4pts)
pdoc="$VAULT_SETUP/scripts/process_docs_to_obsidian.py"
if [ -f "$pdoc" ]; then
if grep -q 'errors="replace"' "$pdoc" && ! grep -qi "warn\|log.*encod\|print.*encod" "$pdoc"; then
add 0 4 "process_docs silently replaces encoding errors (no warning)"
elif grep -qi "warn\|log.*encod\|print.*encod" "$pdoc"; then
add 4 4 "process_docs warns on encoding issues"
else
add 2 4 "process_docs has basic encoding handling"
fi
else
add 0 4 "process_docs_to_obsidian.py not found"
fi
# ═══════════════════════════════════════════
# PORTABILITY (15 pts)
# ═══════════════════════════════════════════
# No absolute paths containing /Users/ in markdown docs (5pts)
users_count=0
for skill_dir in "$VAULT_SETUP" "$DAILY" "$TLDR"; do
count=$(grep -r "/Users/" "$skill_dir" --include="*.md" 2>/dev/null | wc -l | tr -d ' ')
users_count=$((users_count + count))
done
if [ "$users_count" -eq 0 ]; then
add 5 5 "No /Users/ absolute paths in markdown"
elif [ "$users_count" -le 2 ]; then
add 2 5 "$users_count /Users/ paths found in markdown"
else
add 0 5 "$users_count /Users/ paths in markdown"
fi
# Vault path is configurable (5pts)
# Check if tldr-skill uses a convention rather than hardcoded path
if [ -f "$TLDR/SKILL.md" ]; then
if grep -qi "CLAUDE.md\|env.var\|configurable\|vault.root.*determined\|current.working" "$TLDR/SKILL.md" && \
! grep -q "\.obsidian/vaults/\.obsidian-barbosa" "$TLDR/SKILL.md"; then
add 5 5 "tldr-skill vault path is configurable"
elif grep -q "\.obsidian/vaults/\.obsidian-barbosa" "$TLDR/SKILL.md"; then
add 0 5 "tldr-skill has hardcoded vault path"
else
add 3 5 "tldr-skill vault path partially configurable"
fi
else
add 0 5 "tldr-skill SKILL.md not found"
fi
# Cross-platform notes present (5pts)
platform_notes=0
for skill_dir in "$VAULT_SETUP" "$DAILY" "$TLDR"; do
if grep -rqi "cross.platform\|darwin\|linux\|windows\|macos\|platform" "$skill_dir" --include="*.md" --include="*.py" 2>/dev/null; then
platform_notes=$((platform_notes + 1))
fi
done
case $platform_notes in
3) add 5 5 "All skills have cross-platform awareness" ;;
2) add 3 5 "$platform_notes/3 skills have cross-platform content" ;;
1) add 2 5 "$platform_notes/3 skills have cross-platform content" ;;
*) add 0 5 "No cross-platform content found" ;;
esac
# ═══════════════════════════════════════════
# TESTING (10 pts)
# ═══════════════════════════════════════════
# test_vaultbuilder.py exists with at least 3 test functions (5pts)
test_vb="$VAULT_SETUP/scripts/test_vaultbuilder.py"
if [ -f "$test_vb" ]; then
test_count=$(grep -c "def test_" "$test_vb" 2>/dev/null || echo 0)
if [ "$test_count" -ge 3 ]; then
add 5 5 "test_vaultbuilder.py has $test_count tests"
elif [ "$test_count" -ge 1 ]; then
add 3 5 "test_vaultbuilder.py has $test_count tests (need 3+)"
else
add 1 5 "test_vaultbuilder.py exists but no test functions"
fi
else
add 0 5 "test_vaultbuilder.py NOT FOUND"
fi
# test_process_docs.py exists with at least 2 test functions (5pts)
test_pd="$VAULT_SETUP/scripts/test_process_docs.py"
if [ -f "$test_pd" ]; then
test_count=$(grep -c "def test_" "$test_pd" 2>/dev/null || echo 0)
if [ "$test_count" -ge 2 ]; then
add 5 5 "test_process_docs.py has $test_count tests"
elif [ "$test_count" -ge 1 ]; then
add 3 5 "test_process_docs.py has $test_count tests (need 2+)"
else
add 1 5 "test_process_docs.py exists but no test functions"
fi
else
add 0 5 "test_process_docs.py NOT FOUND"
fi
# ═══════════════════════════════════════════
# REPORT
# ═══════════════════════════════════════════
echo "═══ Obsidian Skill Quality Score ═══"
echo -e "$DETAILS"
echo ""
echo "TOTAL: $SCORE / 100"
echo "$SCORE"
#!/usr/bin/env python3
"""Unit tests for process_docs_to_obsidian.py — uses only stdlib unittest."""
import tempfile
import unittest
from pathlib import Path
from process_docs_to_obsidian import add_frontmatter, import_files
class TestAddFrontmatter(unittest.TestCase):
"""Tests for frontmatter injection."""
def test_adds_frontmatter_to_plain_content(self):
result = add_frontmatter("# Hello", Path("test.md"))
self.assertTrue(result.startswith("---"))
self.assertIn("status: inbox", result)
self.assertIn("# Hello", result)
def test_preserves_existing_frontmatter(self):
content = "---\ntitle: existing\n---\n# Hello"
result = add_frontmatter(content, Path("test.md"))
self.assertEqual(result, content)
def test_includes_source_path(self):
result = add_frontmatter("content", Path("/some/source.md"))
self.assertIn("/some/source.md", result)
class TestImportFiles(unittest.TestCase):
"""Tests for the file import logic."""
def test_imports_markdown_files(self):
with tempfile.TemporaryDirectory() as src, tempfile.TemporaryDirectory() as vault:
# Create a test markdown file
(Path(src) / "test.md").write_text("# Test note")
stats = import_files(Path(src), Path(vault))
self.assertEqual(stats["imported"], 1)
self.assertTrue((Path(vault) / "inbox" / "test.md").exists())
def test_skips_existing_files(self):
with tempfile.TemporaryDirectory() as src, tempfile.TemporaryDirectory() as vault:
(Path(src) / "test.md").write_text("# Test")
inbox = Path(vault) / "inbox"
inbox.mkdir()
(inbox / "test.md").write_text("# Already here")
stats = import_files(Path(src), Path(vault))
self.assertEqual(stats["skipped"], 1)
self.assertEqual(stats["imported"], 0)
def test_skips_unsupported_extensions(self):
with tempfile.TemporaryDirectory() as src, tempfile.TemporaryDirectory() as vault:
(Path(src) / "image.png").write_bytes(b"\x89PNG")
stats = import_files(Path(src), Path(vault))
self.assertEqual(stats["imported"], 0)
def test_creates_inbox_directory(self):
with tempfile.TemporaryDirectory() as src, tempfile.TemporaryDirectory() as vault:
(Path(src) / "note.md").write_text("content")
import_files(Path(src), Path(vault))
self.assertTrue((Path(vault) / "inbox").is_dir())
if __name__ == "__main__":
unittest.main()
#!/usr/bin/env python3
"""Unit tests for VaultBuilder.py — uses only stdlib unittest."""
import sys
import tempfile
import unittest
from pathlib import Path
# Add parent Tools/ directory to path so we can import VaultBuilder
TOOLS_DIR = Path(__file__).resolve().parent.parent / "Tools"
sys.path.insert(0, str(TOOLS_DIR))
from VaultBuilder import detect_folders, KEYWORD_FOLDER_MAP, BASE_FOLDERS
class TestDetectFolders(unittest.TestCase):
"""Tests for the keyword-to-folder detection logic."""
def test_single_keyword_match(self):
result = detect_folders("pipeline")
self.assertEqual(result, ["runbooks"])
def test_multiple_keywords(self):
result = detect_folders("pipeline, client, research")
self.assertIn("runbooks", result)
self.assertIn("clients", result)
self.assertIn("research", result)
def test_unknown_keyword_returns_empty(self):
result = detect_folders("nonexistent_keyword_xyz")
self.assertEqual(result, [])
def test_empty_string_returns_empty(self):
result = detect_folders("")
self.assertEqual(result, [])
def test_case_insensitive(self):
result = detect_folders("Pipeline, CLOUD")
self.assertEqual(result, ["runbooks"])
def test_results_are_sorted(self):
result = detect_folders("client, pipeline, research")
self.assertEqual(result, sorted(result))
def test_deduplication(self):
# pipeline and infrastructure both map to runbooks
result = detect_folders("pipeline, infrastructure")
self.assertEqual(result.count("runbooks"), 1)
class TestConstants(unittest.TestCase):
"""Tests for module-level constants."""
def test_base_folders_exist(self):
self.assertIn("inbox", BASE_FOLDERS)
self.assertIn("daily", BASE_FOLDERS)
self.assertIn("projects", BASE_FOLDERS)
self.assertIn("archive", BASE_FOLDERS)
def test_keyword_map_not_empty(self):
self.assertGreater(len(KEYWORD_FOLDER_MAP), 10)
def test_all_map_values_are_strings(self):
for key, value in KEYWORD_FOLDER_MAP.items():
self.assertIsInstance(value, str, f"Key '{key}' maps to non-string: {value}")
if __name__ == "__main__":
unittest.main()
VaultBuilder — CLI Reference
Python CLI tool for creating and verifying Obsidian vaults.
Prerequisites
pip install clickCommands
create — Build vault structure
Creates folders and CLAUDE.md based on keyword detection.
python Tools/VaultBuilder.py create \
--role-keywords "devops,cloud,pipeline" \
--scope work \
--vault-path /path/to/vault \
--role-description "DevOps engineer managing Azure infrastructure"Flags:
| Flag | Required | Default | Description |
|---|---|---|---|
--role-keywords | Yes | — | Comma-separated keywords for folder detection |
--scope | No | work | work, personal, or both |
--vault-path | No | . | Path to create vault in |
--role-description | No | — | Free-text description for CLAUDE.md |
--open-obsidian / --no-open-obsidian | No | --open-obsidian | Open vault in Obsidian after creation |
verify — Check vault setup
Runs post-setup verification checks.
python Tools/VaultBuilder.py verify --vault-path /path/to/vaultFlags:
| Flag | Required | Default | Description |
|---|---|---|---|
--vault-path | No | . | Path to vault |
Output: Structured [ok] / [MISSING] / [BROKEN] / [--] status for each check.
inject-global — Safe global context injection
Adds vault path to ~/.claude/CLAUDE.md. Idempotent — safe to run multiple times.
python Tools/VaultBuilder.py inject-global --vault-path /absolute/path/to/vaultFlags:
| Flag | Required | Default | Description |
|---|---|---|---|
--vault-path | Yes | — | Absolute path to vault |
Behavior:
- If vault path already in
~/.claude/CLAUDE.md→ skips ("Already configured") - If
## My Personal Contextsection exists → appends under it - If section doesn't exist → adds section at end
- If file doesn't exist → creates it with the section
Examples
# DevOps engineer, work-only vault
python Tools/VaultBuilder.py create --role-keywords "devops,cloud,pipeline,terraform" --scope work
# Student with personal tracking
python Tools/VaultBuilder.py create --role-keywords "notes,class,study,personal" --scope both --vault-path ~/vaults/school
# Verify an existing vault
python Tools/VaultBuilder.py verify --vault-path ~/vaults/work
# Add vault to global context
python Tools/VaultBuilder.py inject-global --vault-path ~/vaults/work#!/usr/bin/env python3
"""VaultBuilder — CLI tool for creating and verifying Obsidian vaults."""
import json
import os
import platform
import shutil
import subprocess
import sys
from datetime import datetime
from pathlib import Path
try:
import click
except ImportError:
print("Error: click is required. Install with: pip install click", file=sys.stderr)
sys.exit(1)
BASE_FOLDERS = ["inbox", "daily", "projects", "archive"]
KEYWORD_FOLDER_MAP = {
"pipeline": "runbooks",
"infrastructure": "runbooks",
"devops": "runbooks",
"cloud": "runbooks",
"deploy": "runbooks",
"terraform": "runbooks",
"kubernetes": "runbooks",
"client": "clients",
"customer": "clients",
"account": "clients",
"engagement": "clients",
"research": "research",
"evaluate": "research",
"architecture": "research",
"tool": "research",
"compare": "research",
"content": "content",
"blog": "content",
"video": "content",
"podcast": "content",
"newsletter": "content",
"meeting": "meetings",
"standup": "meetings",
"retro": "meetings",
"1-on-1": "meetings",
"personal": "personal",
"life": "personal",
"health": "personal",
"family": "personal",
"finance": "personal",
"notes": "notes",
"class": "notes",
"lecture": "notes",
"course": "notes",
"study": "notes",
"decision": "decisions",
"adr": "decisions",
"tradeoff": "decisions",
"people": "people",
"team": "people",
"reports": "people",
"manager": "people",
"operations": "operations",
"sop": "operations",
"procedure": "operations",
"runbook": "operations",
"sales": "sales",
"deal": "sales",
"crm": "sales",
}
def detect_folders(keywords_csv: str) -> list[str]:
"""Detect additional folders from comma-separated keywords."""
keywords = [k.strip().lower() for k in keywords_csv.split(",")]
detected = set()
for kw in keywords:
if kw in KEYWORD_FOLDER_MAP:
detected.add(KEYWORD_FOLDER_MAP[kw])
return sorted(detected)
def open_obsidian(vault_path: Path) -> None:
"""Open vault in Obsidian, cross-platform."""
system = platform.system()
uri = f"obsidian://open?path={vault_path.resolve()}"
try:
if system == "Darwin":
subprocess.run(["open", "-a", "Obsidian", str(vault_path.resolve())], check=False)
elif system == "Linux":
subprocess.run(["xdg-open", uri], check=False)
elif system == "Windows":
os.startfile(uri)
else:
click.echo(f" [--] Unknown platform '{system}' — open Obsidian manually")
except Exception as e:
click.echo(f" [--] Could not open Obsidian: {e}")
@click.group()
def cli():
"""VaultBuilder — Create and verify Obsidian vaults."""
pass
@cli.command()
@click.option("--role-keywords", required=True, help="Comma-separated keywords (e.g., 'devops,cloud,pipeline')")
@click.option("--scope", type=click.Choice(["work", "personal", "both"]), default="work", help="Vault scope")
@click.option("--vault-path", type=click.Path(), default=".", help="Path to create vault in")
@click.option("--role-description", default="", help="Free-text role description for CLAUDE.md")
@click.option("--open-obsidian/--no-open-obsidian", "should_open", default=True, help="Open vault in Obsidian after creation")
def create(role_keywords, scope, vault_path, role_description, should_open):
"""Create vault folders and CLAUDE.md based on keyword detection."""
vault = Path(vault_path).resolve()
# Detect additional folders
extra_folders = detect_folders(role_keywords)
if scope in ("personal", "both"):
extra_folders = sorted(set(extra_folders) | {"personal"})
all_folders = BASE_FOLDERS + [f for f in extra_folders if f not in BASE_FOLDERS]
# Create folders
click.echo("Creating vault structure:")
for folder in all_folders:
folder_path = vault / folder
folder_path.mkdir(parents=True, exist_ok=True)
click.echo(f" [ok] {folder}/")
# Write CLAUDE.md
claude_md = vault / "CLAUDE.md"
role_line = role_description if role_description else f"Knowledge worker ({', '.join(role_keywords.split(',')[:3])})"
folder_tree = "\n".join(f"{f + '/':20s} — " for f in all_folders)
content = f"""# CLAUDE.md — {role_line}'s Second Brain
## Who I Am
{role_description if role_description else f"I work with {role_keywords}. This vault is my second brain."}
## My Vault Structure
```
{chr(10).join(f'{f}/' for f in all_folders)}
```
## How I Work
- When I capture something, sort it fast — inbox should stay empty
- I prefer concise, actionable documentation
## Context Rules
When something lands in inbox/ → ask if I want it sorted now
"""
claude_md.write_text(content)
click.echo(f" [ok] CLAUDE.md written")
# Open Obsidian
if should_open:
open_obsidian(vault)
click.echo(f" [ok] Obsidian open requested")
click.echo(f"\nVault created at {vault} with {len(all_folders)} folders.")
@cli.command()
@click.option("--vault-path", type=click.Path(exists=True), default=".", help="Path to vault")
def verify(vault_path):
"""Verify vault setup — check directories, CLAUDE.md, symlinks, global config."""
vault = Path(vault_path).resolve()
ok_count = 0
issues = []
click.echo("Verification:")
# Check directories
dirs_found = 0
for item in vault.iterdir():
if item.is_dir() and not item.name.startswith("."):
dirs_found += 1
if dirs_found > 0:
click.echo(f" [ok] {dirs_found} folders found")
ok_count += 1
else:
click.echo(f" [MISSING] No vault folders found")
issues.append("No folders")
# Check CLAUDE.md
claude_md = vault / "CLAUDE.md"
if claude_md.exists():
lines = claude_md.read_text().splitlines()[:5]
role = "vault"
for line in lines:
if line.startswith("# CLAUDE.md"):
role = line.split("—")[-1].strip() if "—" in line else "vault"
break
click.echo(f" [ok] CLAUDE.md written ({role})")
ok_count += 1
else:
click.echo(f" [MISSING] CLAUDE.md not found")
issues.append("No CLAUDE.md")
# Check companion skill symlinks
for skill in ["daily-skill", "tldr-skill"]:
link = vault / ".claude" / "skills" / skill
if link.is_symlink():
if link.resolve().exists():
click.echo(f" [ok] {skill} linked")
ok_count += 1
else:
click.echo(f" [BROKEN] {skill} symlink exists but target missing")
issues.append(f"Broken {skill} symlink")
else:
click.echo(f" [--] {skill} not linked (optional)")
# Check global context
global_claude = Path.home() / ".claude" / "CLAUDE.md"
if global_claude.exists():
content = global_claude.read_text()
if str(vault) in content:
click.echo(f" [ok] Global context configured")
ok_count += 1
else:
click.echo(f" [--] Global context not configured")
else:
click.echo(f" [--] ~/.claude/CLAUDE.md does not exist")
# Check Obsidian running
system = platform.system()
try:
if system == "Darwin":
result = subprocess.run(["pgrep", "-x", "Obsidian"], capture_output=True)
elif system == "Linux":
result = subprocess.run(["pgrep", "-f", "obsidian"], capture_output=True)
else:
result = type("R", (), {"returncode": 1})()
if result.returncode == 0:
click.echo(f" [ok] Obsidian running")
else:
click.echo(f" [--] Obsidian not detected (open manually)")
except Exception:
click.echo(f" [--] Could not check Obsidian status")
@cli.command("inject-global")
@click.option("--vault-path", required=True, type=click.Path(exists=True), help="Absolute path to vault")
def inject_global(vault_path):
"""Safe global context injection — idempotent, won't duplicate."""
vault = Path(vault_path).resolve()
global_claude = Path.home() / ".claude" / "CLAUDE.md"
vault_line = f"At the start of every session, read {vault}/CLAUDE.md for context about who I am, my work, and my conventions."
section_header = "## My Personal Context"
if global_claude.exists():
content = global_claude.read_text()
# Already configured?
if str(vault) in content:
click.echo("Already configured — vault path found in ~/.claude/CLAUDE.md")
return
# Has section? Append under it
if section_header in content:
content = content.replace(section_header, f"{section_header}\n{vault_line}")
global_claude.write_text(content)
click.echo(f"Appended vault path under existing '{section_header}' section.")
else:
# Append section
content = content.rstrip() + f"\n\n{section_header}\n{vault_line}\n"
global_claude.write_text(content)
click.echo(f"Added '{section_header}' section with vault path.")
else:
# Create file
global_claude.parent.mkdir(parents=True, exist_ok=True)
global_claude.write_text(f"{section_header}\n{vault_line}\n")
click.echo(f"Created ~/.claude/CLAUDE.md with vault path.")
if __name__ == "__main__":
cli()
Import Files — Getting Existing Content into Your Vault
Guidance for importing existing documents into the vault. Replaces the previous broken script reference with practical options.
Option 1: Manual Drop (Simplest)
Drop files directly into inbox/:
Supported formats:
.md— Markdown (native, works immediately).pdf— Viewable in Obsidian with built-in PDF viewer.txt— Plain text, rename to.mdfor full features.docx— Not natively supported; convert to Markdown first (see Option 3)
Then tell Claude: "Sort everything in inbox/"
Claude will read each file, infer its topic, and move it to the appropriate vault folder with proper frontmatter.
Option 2: Bulk Import Script
For importing many files at once, use the included script:
python scripts/process_docs_to_obsidian.py ~/path/to/source/files /path/to/vaultWhat it does:
- Copies
.md,.txt,.pdffiles toinbox/ - Adds basic frontmatter to
.mdfiles (created date, source path) - Skips files that already exist in the vault (by filename)
- Reports what was imported
After running, tell Claude: "Sort everything in inbox/"
Option 3: Converting DOCX/PPTX to Markdown
For Word documents and PowerPoints, convert to Markdown before importing:
# Using pandoc (install: brew install pandoc / apt install pandoc)
pandoc document.docx -o document.md
pandoc slides.pptx -o slides.mdThen move the .md files to inbox/.
Option 4: Gemini-Powered Processing
If you have the file-intel-skill installed, it can process entire folders with AI-powered summarization:
/file-intel ~/path/to/folderThis extracts content from PDF, PPTX, XLSX, DOCX, CSV, JSON, and text formats, generating Obsidian-ready summaries.
Tips
- Don't overthink sorting — drop everything in
inbox/first, sort later - Keep originals — the import script copies, not moves
- Large libraries — import in batches of 20-50 files to keep sorting manageable
- Images — put in an
attachments/folder; Obsidian handles image embedding natively
Setup — Main Vault Configuration Flow
Run from INSIDE the folder you want to become your Obsidian vault. Six steps: interview → infer → confirm → build → global context → verify.
Step 1 — User Interview
Display this message exactly, then wait for their response:
---
Tell me about yourself in a few sentences so I can build your vault.
Answer these in whatever order feels natural:
- What do you do for work?
- What falls through the cracks most — what do you wish you tracked better?
- Work only, or personal life too?
- Do you have existing files to import? (PDFs, docs, slides)
No need to be formal. A few sentences is enough.
---
Step 2 — Keyword-Driven Inference
From their free-text answer, detect keywords and infer vault structure. Do NOT ask clarifying questions — make smart inferences.
1. Read RoleTemplates.md for the keyword-to-folder mapping table 2. Scan the user's response for matching keywords 3. Base folders are always included: inbox/, daily/, projects/, archive/ 4. Add folders for each keyword match (deduplicated) 5. Infer a role summary (1-2 sentences) from their description 6. Check RoleTemplates.md companion skill recommendations for their domain
Show the preview:
Here's your vault — ready to build when you are.
📁 [current directory name]
├── inbox/ Drop zone — everything new lands here first
├── daily/ Daily brain dumps and quick captures
├── [folder]/ [purpose from RoleTemplates.md]
├── [folder]/ [purpose from RoleTemplates.md]
├── projects/ Active work with status and next actions
└── archive/ Completed work — never deleted, just moved
CLAUDE.md will describe you as:
[inferred role summary from their words]
Companion skills available:
daily-skill — start your day with vault context
tldr-skill — save session summaries to the right folder
[domain-specific skills from RoleTemplates.md]
Recommended plugins: [top 4 from PluginRecommendations.md]
Type "build it" to create this, or tell me what to change.Wait for confirmation before proceeding.
Step 3 — Confirmation
Accept: "build it", "yes", "go", "looks good", or similar affirmative.
If they request changes, adjust the preview and show it again. Do NOT proceed until confirmed.
Step 4 — Build
Create folders
mkdir -p inbox daily [detected-role-folders] projects archiveOpen in Obsidian (cross-platform)
case "$(uname -s)" in
Darwin) open -a Obsidian "$(pwd)" ;;
Linux) xdg-open "obsidian://open?path=$(pwd)" ;;
MINGW*|MSYS*) start "obsidian://open?path=$(pwd)" ;;
esacWrite CLAUDE.md
Write directly to CLAUDE.md in the current directory. Use the user's actual words for the role description:
# CLAUDE.md — [inferred role]'s Second Brain
## Who I Am
[2-3 sentences based on what they told you — specific, personal, written in
first person as Claude describing its owner]
## My Vault Structure[folder tree with one-line purpose per folder]
## How I Work
[3-4 bullet points from RoleTemplates.md "How I Work" section, adapted to
match the user's specific description]
## Context Rules
[Context rules from RoleTemplates.md based on detected folders]
When something lands in inbox/ → ask if I want it sorted nowLink companion skills
Instead of creating stub skill files, offer to symlink the real skills from the skills repo:
# Only if the user has claude-code-skills repo
SKILLS_REPO="$HOME/Repos/github/claude-code-skills/skills"
if [ -d "$SKILLS_REPO" ]; then
mkdir -p .claude/skills
ln -sf "$SKILLS_REPO/daily-skill" .claude/skills/daily-skill
ln -sf "$SKILLS_REPO/tldr-skill" .claude/skills/tldr-skill
echo "Linked daily-skill and tldr-skill from your skills repo."
else
echo "Companion skills available at: https://github.com/[repo]/claude-code-skills"
echo "Install them later with: ln -s /path/to/skills/daily-skill .claude/skills/daily-skill"
fiDo NOT create stub skill files. Do NOT create a memory.md file — Claude Code's built-in memory system handles this.
Step 5 — Safe Global Context Injection
Ask the user:
One last thing — how do you want your vault context loaded into Claude Code?
1. Global (recommended) — adds one line to ~/.claude/CLAUDE.md so your vault
context loads automatically in every Claude Code session on this machine
2. Manual — I'll give you the line to paste into specific projects when you need it
3. Vault only — works automatically when you run claude from inside this folderIf global (option 1):
Idempotent injection — safe to run multiple times:
1. Check if ~/.claude/CLAUDE.md exists 2. Search for vault path already referenced → skip if found ("Already configured") 3. Search for ## My Personal Context section:
- If found → append vault path line under it
- If not found → append the section at end of file
4. If no file exists → create with just the section
The line to inject:
At the start of every session, read [absolute vault path]/CLAUDE.md for context about who I am, my work, and my conventions.If manual (option 2): Show the line for them to paste.
If vault only (option 3): No action needed — skip.
Or use the CLI tool:
python Tools/VaultBuilder.py inject-global --vault-path "$(pwd)"Step 6 — Verification
Run Workflows/Verify.md to confirm everything was created correctly.
Then show:
Done. Your vault is live.
Your slash commands (if companion skills were linked):
/daily — run this tomorrow morning
/tldr — run this at the end of any session
Have files to import?
See Workflows/ImportFiles.md or just drop files in inbox/ and say "sort my inbox"
Recommended plugins (install via Obsidian Settings → Community Plugins):
[top 4 from PluginRecommendations.md for their role]Verify — Post-Setup Verification
Run after vault setup to confirm everything was created correctly. Called by Setup.md Step 6, or run independently to check an existing vault.
Checks
1. Directory Structure
List all expected directories. For each, check existence and report status.
for dir in inbox daily projects archive [detected-role-folders]; do
if [ -d "$dir" ]; then
echo " [ok] $dir/"
else
echo " [MISSING] $dir/"
fi
done2. CLAUDE.md
Verify CLAUDE.md exists in the vault root. Show first 5 lines as confirmation.
if [ -f CLAUDE.md ]; then
echo " [ok] CLAUDE.md written"
head -5 CLAUDE.md
else
echo " [MISSING] CLAUDE.md not found"
fi3. Companion Skill Symlinks
Check if any companion skills were symlinked and whether the symlinks are valid (not broken).
for skill in daily-skill tldr-skill; do
link=".claude/skills/$skill"
if [ -L "$link" ]; then
if [ -e "$link" ]; then
echo " [ok] $skill linked"
else
echo " [BROKEN] $skill symlink exists but target missing"
fi
else
echo " [--] $skill not linked (optional)"
fi
done4. Global Context
If the user chose global context injection, verify ~/.claude/CLAUDE.md contains the vault path.
vault_path="$(pwd)"
if [ -f ~/.claude/CLAUDE.md ]; then
if grep -q "$vault_path" ~/.claude/CLAUDE.md; then
echo " [ok] Global context configured"
else
echo " [--] Global context not configured (vault path not found in ~/.claude/CLAUDE.md)"
fi
else
echo " [--] ~/.claude/CLAUDE.md does not exist"
fi5. Obsidian Running
Check if Obsidian is running (informational only — not a failure if it isn't).
case "$(uname -s)" in
Darwin) pgrep -x Obsidian > /dev/null && echo " [ok] Obsidian running" || echo " [--] Obsidian not detected (open manually)" ;;
Linux) pgrep -f obsidian > /dev/null && echo " [ok] Obsidian running" || echo " [--] Obsidian not detected (open manually)" ;;
MINGW*|MSYS*) tasklist | grep -qi obsidian && echo " [ok] Obsidian running" || echo " [--] Obsidian not detected (open manually)" ;;
esacOutput Format
Combine all checks into a single summary block:
Verification:
[ok] 7 folders created
[ok] CLAUDE.md written (DevOps / Cloud Engineer)
[ok] daily-skill linked
[ok] tldr-skill linked
[ok] Global context configured
[--] Obsidian not detected (open manually)Status indicators:
[ok]— check passed[MISSING]— required item not found (setup may have failed)[BROKEN]— item exists but is invalid (e.g., broken symlink)[--]— optional item not present (informational, not an error)