AgentStack
SKILL verified MIT Self-run

Obsidian Graph Audit

skill-lexbritvin-obsidian-skills-pack-obsidian-graph-audit · by lexbritvin

Diagnose Obsidian Graph view health and clean up over-connection — identify render-only edges (dashboards as topics, log files), mega-categories, broken refs, and false categories that are inflating the graph. Use whenever the user reports the graph feels tangled, overconnected, hairball-like, or noisy, even if they don't explicitly say "audit". Triggers include "why is my graph a hairball", "aud…

No reviews yet
0 installs
0 views
view→install

Install

$ agentstack add skill-lexbritvin-obsidian-skills-pack-obsidian-graph-audit

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access Used
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Obsidian Graph Audit? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Obsidian Graph Audit

A diagnostic workflow for an Obsidian graph that feels tangled, overconnected, or visually noisy. Operates well on Kepano-style vaults (with categories: and topics: in frontmatter) but the patterns generalize.

This skill diagnoses and recommends. It does not silently apply fixes. The deliverable is a report — pain points, ranked by impact, with proposed actions the user can approve, modify, or skip. Mitigation scripts exist (later in this file) but only run after the user explicitly confirms a specific action. Color tweaks and filter changes are out of scope — see the obsidian-graph skill for styling.

When to use

User says:

  • "Graph is a hairball / spaghetti / mess"
  • "Too many connections, can't read it"
  • "Why is X linked to Y? They aren't related"
  • "Help me clean up the graph"
  • "Find what's making the graph noisy"
  • "What should I exclude from the graph?"

Prerequisites

The diagnostic one-liners below use rg (ripgrep), awk, find, and python3. All are common on macOS and Linux except rg, which usually requires brew install ripgrep or equivalent. If rg is not available, substitute grep -c (for counts) or grep -E (for patterns) — slightly slower on large vaults but otherwise equivalent.

Core insight

A node draws visual attention proportional to its degree (number of edges). High-degree nodes are one of:

  • Real semantic hubs — coordinating concepts (epics, MOCs, topics-of-many-notes). Keep.
  • Render-only hubs — accumulators of inbound links for view rendering (dashboards, log files with inline-fields). Remove from graph or convert.
  • Mega-categories — categories used as "tag for files in this folder" rather than entity types. Refactor.
  • Broken refs — placeholders, malformed wikilinks creating ghost nodes. Fix.

Audit's job is to classify each visible hub into one of those buckets.

Workflow

The audit is two phases: diagnose + recommend (steps 1–5), then act on confirmation (step 6). Never skip phase 1. Never run phase 2 without an explicit go-ahead from the user on the specific action.

1. Establish baseline

find . -name "*.md" -not -path "./.obsidian/*" -not -path "./.trash/*" -not -path "./.claude/*" | wc -l

Vaults under ~200 notes don't accumulate the patterns this skill targets. Skip the audit; the graph is just sparse.

2. List top out-degree files

find . -name "*.md" -not -path "./.obsidian/*" -not -path "./.trash/*" -not -path "./.claude/*" -print0 | \
  while IFS= read -r -d '' f; do
    count=$(rg -c '\[\[' "$f" 2>/dev/null || echo 0)
    echo "$count	$f"
  done | sort -rn | head -20

Approximate — counts wikilinks anywhere in the file, including inside code blocks. Good enough for triage; verify suspect files individually.

3. List top in-degree (most-linked-to) targets

Frontmatter-only count of topics: references (more accurate than free-text grep):

find . -name "*.md" -not -path "./.obsidian/*" -not -path "./.trash/*" -not -path "./.claude/*" -print0 | \
  while IFS= read -r -d '' f; do
    awk 'BEGIN{s=0} /^---$/{s++; next} s==1 && /^topics:/{p=1; next} s==1 && p && /^[a-zA-Z_]+:/{p=0} s==1 && p' "$f" | \
      rg -o '\[\[([^\]|#]+)' -r '$1'
  done | sort | uniq -c | sort -rn | head -20

The state-counter (s++) is critical: parsing only between the first and second --- avoids walking into body horizontal rules and miscounting.

Same idea for categories — replace topics: with categories: in the awk pattern.

4. Classify each top hub

For each node in the top-20:

| Type | Signature | Recommended action | |---|---|---| | Real semantic hub (epic, MOC, topic) | Has its own file, file is short and coordinates other notes via lists/links | Keep | | Index page for a category (Recipes.md, Companies.md) | Hub-file with tag: hub listing members | Keep | | Dashboard / view page | Renders queries (Dataview, Bases, charts); inbound topics are render-only | Tag #dashboard, exclude | | Log file with inline-fields | Append-only file with [key:: value] bullets; consumed by Dataview | Tag #no-graph, exclude | | Mega-category | Category applied to all files in one folder; folder ≈ category | Convert to topic | | False category | Project/folder name used as category | Remove | | Broken placeholder | Template-leaked node like [[]] or [[{{Y}}]] | Edit out (verify first — may be inside a code block, false alarm) |

5. Present the report (always)

Deliver to the user before any action:

  1. Pain points — the top-degree nodes that are not real semantic hubs, classified into the table above. State what the problem is in one sentence per finding.
  2. Recommendations — for each pain point, propose an action with the expected impact ("strip topic Dashboard - X from 26 files, removes 26 edges from a 50-edge fake hub").
  3. Counts and scope — how many files each action touches, which folders are affected.
  4. Open questions — anything ambiguous the user must decide (e.g. a tag like john that maps to multiple entities; a meta-category that could be soft-hidden or hard-removed).
  5. Action plan, ordered by impact — high-leverage fixes first, low-cost cleanups later.

Format the report so the user can scan it and reply "do A and C, skip B, ask me again about D". Do not lump actions together — each one is a separate go/no-go decision.

6. Act on confirmation only

Only after the user explicitly approves a specific action, run the corresponding mitigation pattern (see Mitigation patterns below) with --dry-run first, show the dry-run output, then --apply. If the user approved A but the dry-run shows it also touches files outside the expected scope, pause and report — do not proceed on the prior approval.

Diagnostic patterns

A. Mega-category from folder-as-category

A category is applied to nearly every file in one folder, accumulating 30+ inbound edges to the category page.

Signature: categories: [[X]] on N files where N is comparable to the folder's file count, and the category name matches the folder name.

Why noise: Files already share the folder's structural identity. The category-as-page collects them into a star pattern that says nothing the path didn't already.

Fix: Demote the category to a topic. categories: is for entity type ("this is a Company"); topics: is for thematic association ("this is about Onboarding Course"). Bulk-rewrite the frontmatter.

Diagnostic:

# Files with category X grouped by parent folder
CAT=""
find . -name "*.md" -not -path "./.obsidian/*" -print0 | \
  while IFS= read -r -d '' f; do
    awk 'BEGIN{s=0} /^---$/{s++; next} s==1 && /^categories:/{p=1; next} s==1 && p && /^[a-zA-Z_]+:/{p=0} s==1 && p' "$f" | \
      rg -q "\\[\\[$CAT\\]\\]" && dirname "$f"
  done | sort | uniq -c | sort -rn

If 80%+ are in one folder — it's a mega-category candidate.

B. Render-only edges (dashboards as topics)

Files have topics: [[Dashboard - X]] or similar, where the "topic" is a view-page that renders queries. The topic doesn't carry semantic meaning — it tells the file where it gets displayed, not what it's about.

Signature: A file named Dashboard - X (or similar view-page) appears as a topic on many other files. The dashboard itself contains Dataview/Bases queries, filtering by path or tag — not by topic.

Why noise: N files → 1 dashboard = N edges with zero navigational value. You don't jump from a contact to a dashboard to learn about the contact; you go the other way (dashboard lists contacts).

Fix: Strip the dashboard topic from those files. The dashboard's own queries (dv.pages('"path/X"')) keep working — they never used the topic-edge for filtering.

Diagnostic:

TARGET="Dashboard - X"
find . -name "*.md" -not -path "./.obsidian/*" -print0 | \
  while IFS= read -r -d '' f; do
    awk 'BEGIN{s=0} /^---$/{s++; next} s==1 && /^topics:/{p=1; next} s==1 && p && /^[a-zA-Z_]+:/{p=0} s==1 && p' "$f" | \
      rg -q "\\[\\[$TARGET\\]\\]" && echo "$f"
  done

C. Log files with inline-field metadata

A single file holds bullets like - [name:: X] [author:: [[Y]]] [series:: [[Z]]] → [[Linked]]. Each bullet creates 2-4 wikilink edges. With 30+ bullets the log becomes a 50+-degree fake hub.

Signature: File is append-only, mostly bullets with [key:: value] inline-fields. Consumed as a dataset by Dataview queries elsewhere in the vault.

Why noise: This is structured data, not conceptual content. Edges represent rows, not associations.

Fix: Tag the file #no-graph (or #log) and exclude via filter. Dataview/Bases queries pulling its data still work — they read the file directly, not the graph.

Diagnostic:

# Files with high inline-field density
find . -name "*.md" -not -path "./.obsidian/*" -print0 | \
  while IFS= read -r -d '' f; do
    count=$(rg -c '\[[a-z_]+::' "$f" 2>/dev/null || echo 0)
    [ "$count" -gt 10 ] && echo "$count	$f"
  done | sort -rn

D. False category (project name as category)

A file has categories: [[Project Name]], where "Project Name" is a folder/epic, not an entity type. Categories should answer "what kind of thing is this", not "which project does this belong to".

Signature: Few files use this category (1-3). Category name matches a folder or epic.

Why noise: Creates a stray category-page node tied to those few files, masquerading as a real entity-class hub.

Fix: Remove the false category. If thematic association is needed, add a topics: entry instead.

Diagnostic:

# Categories with low usage — likely false or singletons
find . -name "*.md" -not -path "./.obsidian/*" -print0 | \
  while IFS= read -r -d '' f; do
    awk 'BEGIN{s=0} /^---$/{s++; next} s==1 && /^categories:/{p=1; next} s==1 && p && /^[a-zA-Z_]+:/{p=0} s==1 && p' "$f"
  done | rg -o '\[\[([^\]]+)\]\]' -r '$1' | sort | uniq -c | sort -n | head -20

E. Broken placeholders (template leakage)

Wikilinks like [[]], [[{{var}}]], or [[Title|alias]]\ (escaped pipe) appear in topics: or categories:. Created when a template was copied without substituting variables.

Signature: Wikilink content contains `, {{, }}, \|`, or other template syntax. Often single-occurrence.

Why noise: Creates ghost or unresolved nodes, adding fake centrality.

Fix: Inspect each — usually edit the line out. Check carefully: a placeholder inside a code block is intentional documentation (~~~markdown example showing template format) — Obsidian doesn't create graph edges from code blocks. False alarm.

Diagnostic:

rg -l '\[\[.md` (or close variants) in the vault.

**Why noise:** A flat tag doesn't create a graph edge. A property field with a `[[wikilink]]` already does. So the tag is duplicate metadata that pollutes frontmatter without adding graph signal. It also accumulates a tag taxonomy of one-off entity nouns, which makes search-by-tag less useful (the long tail floods the tag list).

**Fix — two cases:**
- **Property field already has the entity wikilink** → remove the tag (it's a pure duplicate).
- **No property field reference** → remove the tag and add `topics: [[Entity Name]]`. This creates the graph edge that was missing.

This is parallel to Pattern A (mega-category from folder-as-category): both patterns address frontmatter that uses one mechanism when another mechanism would carry more semantic weight.

**Diagnostic — find candidates:**
```bash
# List all tags by frequency. Entity-name tags will have noun-like values
# (author names, person names, place names) rather than category-like values
# (interview, homework, status/done).
find . -name "*.md" -not -path "./.obsidian/*" -print0 | \
  while IFS= read -r -d '' f; do
    awk 'BEGIN{s=0} /^---$/{s++; next} s==1 && /^tags:/{p=1; next} s==1 && p && /^[a-zA-Z_]+:/{p=0} s==1 && p' "$f"
  done | sed -e 's/^[ -]*//' -e 's/^\[//' -e 's/\]$//' -e 's/, /\n/g' | \
  sort | uniq -c | sort -rn | head -50

For each suspect tag value, check if a matching *.md file exists. If yes, it's a candidate for conversion.

Diagnostic — for one specific tag:

TAG="tolkien"
ENTITY="J. R. R. Tolkien"   # matching file basename
find . -name "*.md" -not -path "./.obsidian/*" -print0 | \
  while IFS= read -r -d '' f; do
    has_tag=$(awk 'BEGIN{s=0} /^---$/{s++; next} s==1' "$f" | rg -q "\b$TAG\b" && echo Y || echo N)
    has_wl=$(awk 'BEGIN{s=0} /^---$/{s++; next} s==1' "$f" | rg -q "\[\[$ENTITY\]\]" && echo Y || echo N)
    [ "$has_tag" = Y ] && echo "tag=$has_tag  wl=$has_wl  $f"
  done

Files with tag=Y wl=Y → tag is duplicate, just remove. Files with tag=Y wl=N → convert to topic.

Caveat — preserve functional tags: Some entity-name-like tags are intentionally used by Dataview/Bases queries for filtering. Before bulk-converting, search the vault for WHERE contains(tags, "") or tags:contains("") — if a query depends on the tag, removing it breaks the view. Confirm tag-by-tag before refactoring.

Disambiguation: A tag like john may map to multiple entities (John Smith.md vs John Doe.md). Resolve per-file by inspecting frontmatter (author:, attendees:, interviewer: fields), co-occurring tags, or body content. Don't bulk-rewrite ambiguous tags without classification.

Caveat — exclude Templates/ from bulk runs. Templates may contain interpolated placeholders that look like entity tags but are actually template syntax. Common forms:

  • Obsidian core templates: {{date:MMMM}}, {{title}} — replaced when a note is created from the template
  • Templater plugin: `, `
  • Others depending on plugins

A tag like plan/{{date:MMMM}} looks broken but is intentional — it expands to plan/May, plan/June etc. on note creation. Removing it from a template breaks all future notes generated from it.

Always exclude Templates/ from any bulk script unless template hygiene is explicitly the goal:

dirnames[:] = [d for d in dirnames if d not in (".obsidian", ".trash", ".claude", "Templates")]

Mitigation patterns

These run only after the user has approved a specific action during step 5. They are reference recipes, not a default sequence — never run a mitigation just because a pattern was detected. The user picks which pain points to act on.

Pre-flight checklist before --apply

Before running any bulk frontmatter rewrite, do these four checks. Skipping them is how silent damage happens (a real example: a cleanup script removed plan/{{date:MMMM}} from three template files because the script walked all folders by default; the placeholder is intentional Obsidian template syntax, and stripping it broke note creation).

  1. Run dry-run first. The script must print, not write, by default. Apply only after the printed report looks right.
  2. Group affected files by top-level folder. A one-liner on the dry-run output:

``bash python3 cleanup.py | rg '^\./[^/]+/' -o | sort | uniq -c | sort -rn ` If Templates/, Attachments/`, or any vault internal folder appears — pause. Templates contain intentional placeholders that look like noise. Plugins may store config in folders you don't recognize.

  1. Inspect three random affected files inline. Read the current frontmatter of three files the dry-run reports as modified. Confirm the planned change is correct for each. If even one looks wrong, the rule is wrong, not the file.
  2. Exclude Templates/ unless template hygiene is the goal. This is the default for any bulk script — interpolated placeholders ({{date:MMMM}}, ``, etc.) look like broken refs but are required for new-note creation:

``python dirnames[:] = [d for d in dirnames if d not in (".obsidian", ".trash", ".claude", "Templates")] ``

Only after all four checks pass, run with --apply. If a problem surfaces post-apply, git checkout -- reverts cleanly because every change is

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.