vault-cleanup-studio
Find vault hygiene issues — stub notes, oversized notes, duplicates, stale attachments — and fix them ONLY after explicit permission per change. Use when the user says "clean up the vault", "find junk
SKILL.mdskills/vault-cleanup-studio/SKILL.md
GitHubCleanup Studio
Vault hygiene analysis tool. Read-only by design — always asks user permission before any action.
Golden Rules
NEVER auto-fix. ALWAYS ask permission.
When reporting issues, Claude Code must:
- Show the issue
- Explain options
- Ask "Would you like me to [action]?"
- Wait for explicit confirmation
Before deleting files, ALWAYS check recoverability:
- Is the file tracked by git? (
git ls-files <path>) - Is the file in the daily backup? (Backup excludes
.mp4,.wav,.mov— seebackup-vault.bat) - If neither git nor backup → warn the user: "These files are NOT recoverable if deleted."
- For batch deletions (>5 files), list the count and total size, and get explicit "yes delete" confirmation.
Files
- Script:
80_Code/scripts/cleanup-studio.py— content hygiene (this skill's detector) - Sibling scanner:
D:/emptyos/scripts/check_vault_structure.py— structural drift (NOT this skill)
Division of labour — don't re-derive what a scanner already decides
Two scanners cover different axes. Run the structural one first; it settles the mechanical cases deterministically so the triage below only handles judgment.
| Class | Owner | Why |
|---|---|---|
Empty dirs, byte-identical sync-conflict copies (name 2.md), .tmp, aged zero-byte files |
check_vault_structure.py |
Content-lossless, classified safe, auto-purgeable |
Undeclared top-level folders, divergent duplicate copies, non-empty Untitled*.md |
check_vault_structure.py (review class) |
It reports; a human decides. Never auto-touched |
| Oversized notes (raw size threshold) | check_vault_structure.py (advisory) |
Mechanical size check |
| Stub categorization (old-journal vs dictionary vs truly-empty) | this skill | Needs domain judgment |
| Huge-note expected-vs-actionable classification | this skill | Needs to know what an OCR'd standard is |
| Duplicate-name rename convention + wikilink updates | this skill | Needs naming taste + link surgery |
| Stale-attachment keep/delete calls | this skill | Needs recoverability + intent |
# Structural pass first (report-only; --purge is a human-run act, never automatic)
python D:/emptyos/scripts/check_vault_structure.py
Exit code = number of review findings. If it flags a duplicate as byte-identical,
don't re-triage it below — it's already settled. Only bring pairs it calls
divergent into the Duplicates workflow.
Commands
Run from vault root with PYTHONIOENCODING=utf-8 on Windows:
# Quick summary
PYTHONIOENCODING=utf-8 python "80_Code/scripts/cleanup-studio.py"
# Individual checks
PYTHONIOENCODING=utf-8 python "80_Code/scripts/cleanup-studio.py" --stubs
PYTHONIOENCODING=utf-8 python "80_Code/scripts/cleanup-studio.py" --huge
PYTHONIOENCODING=utf-8 python "80_Code/scripts/cleanup-studio.py" --duplicates
PYTHONIOENCODING=utf-8 python "80_Code/scripts/cleanup-studio.py" --stale
# Full lists (no truncation)
PYTHONIOENCODING=utf-8 python "80_Code/scripts/cleanup-studio.py" --stubs --all
Smart Filtering (What the Script Already Handles)
The script automatically excludes false positives. Do NOT flag these as issues:
Excluded from all scans
Folders: .obsidian, .git, .claude, _claude, .claude_backup, node_modules, .pytest_cache, venv, venv_mac, __pycache__, .semantic_index, .smart-connections, .makemd, .space, .trash, temp-backup
Excluded from stubs
These are intentionally short notes, not real stubs:
$-prefix notes (finance tracking)#subscriptiontagged notes- Credential/API key notes (passwords, tokens, GitHub/Groq/OpenAI keys)
- Finance keywords in filename or body (税号, 信用卡, 银行, etc.)
- Pure-number bodies (account numbers, IDs)
- Attachment-only notes (just
![[...]]embeds) - Completed-task-only notes (only
- [x]items) - Social media account lists
Excluded from duplicates
- Structural convention names:
_tracker(DB Folder plugin index)
Triage Workflow
Stubs
After running --stubs, categorize results before acting:
| Category | Typical action |
|---|---|
| Old journal entries (2015 etc.) | Keep — historical, just short days |
| Dictionary entries | Keep — valid vocab notes |
| Recipes | Keep — short recipes are fine |
| Archived notes | Keep — cold storage reference |
| Work notes with just headings | Expand or keep as placeholder |
| Truly empty / meaningless | Delete after confirming |
Huge Notes
The --huge output groups notes by category and shows a breakdown. Most huge notes are expected to be large:
| Category | Expected? | Action |
|---|---|---|
| Cable rating references (OCR'd standards) | Yes | Keep — splitting loses context |
| HV sheath voltage papers (OCR'd) | Yes | Keep |
| University notes | Yes | Keep — completed coursework |
| English/PTE study banks | Yes | Keep — inherently large (机经) |
| Excalidraw drawings | Yes | Keep — binary-ish format |
| Book notes / Readwise | Yes | Keep — long-form content |
| Archived notes | Yes | Keep — already in archive |
| CBT/therapy logs | Yes | Keep — personal journals |
| Notification log | No | Trim — _claude-notifications.md grows unbounded |
| Other | Check | These are the actionable ones shown in detail |
Only the "Actionable" section of the output needs review.
Duplicates
After running --duplicates, investigate each pair. Skip any pair
check_vault_structure.py already classified as a byte-identical copy — that's
safe-class structural litter, not a naming decision.
- Read both files — are they identical or different?
- If identical → keep canonical copy, replace other with a link
- If different content, same name → rename with suffix/prefix:
- Standard sections:
{standard}--{section}.md(e.g.,tb880--01-introduction.md) - Project-scoped:
{name}-{project}.md(e.g.,README-atp-emtp.md) - YouTube publishing vs creative: add
-ytsuffix - Job applications: add
-{company}suffix
- Standard sections:
- If different purpose, same name by convention → add to
STRUCTURAL_DUPLICATE_NAMESin script - After renaming: always search for and update wikilinks (
Grep+Edit)
Stale Attachments
- Large files (>100KB): confirm with user before deleting
- Small files: can batch-review
- When in doubt, keep — storage is cheap
Temp Backups
The temp-backup/ folder is excluded from all scans. These are safety nets for file modifications. Do not clean them up — the verification cost (reading + diffing each pair) far exceeds the storage cost (~750 KB).
Example Interaction
User: Clean up my vault
Claude: I'll scan for issues first.
[Runs --summary]
CLEANUP SUMMARY
Stub notes: 31
Huge notes: 166
Duplicate names: 1
Stale attachments: 125 (45 MB)
Which would you like to address first?
1. Review stub notes (31 — after smart filtering)
2. Review huge notes (166 — mostly expected, I'll show category breakdown)
3. Review duplicate names (1)
4. Review stale attachments (125, ~45 MB)
User: 1
Claude: [Runs --stubs, categorizes results, asks about each group]
Proactive Behavior
Claude Code should:
- Offer cleanup analysis during weekly reviews
- Warn about duplicate names when creating new notes
- But NEVER delete/modify without asking
Obsidian CLI (Quick Health Checks) — with Fallbacks
Try CLI for instant vault health metrics before running the full Python scan.
OBS="bash 80_Code/scripts/obs.sh"
# Quick health snapshot
$OBS vault # total files, folders, size
$OBS orphans total # notes with no incoming links
$OBS deadends total # notes with no outgoing links
$OBS unresolved total # broken links count
# Find what links to a stub before deciding to delete
$OBS backlinks file="Stub Note Name"
$OBS backlinks file="Stub Note Name" total
Fallbacks (if Obsidian not running):
- Vault stats →
Glob **/*.md+ count - Orphans →
python "80_Code/scripts/generate-link-index.py" --orphans - Broken links →
python "80_Code/scripts/generate-link-index.py" --broken - Backlinks for one note →
Grepfor\[\[Note Nameacross vault
Workflow: CLI health check first → Python script for detailed analysis → triage.
Integration with Other Skills
- Use CLI
backlinks(or Grep fallback) to check links before deleting stubs - After cleanup, regenerate
link-index.json