hanamizuki47 downloadsVisualize AI agent skill structures in graph view. Renames SKILL.md nodes from frontmatter, draws reference edges, and colors nodes by type.
An Obsidian plugin to visualize OpenClaw / Claude Code agent skill structures in graph view.
https://github.com/user-attachments/assets/2fabeea0-eb33-42d8-ae9d-2bf06fa884f0
In Graph View:
name value (instead of the filename "SKILL")Read-only — no original files are modified. All changes are in-memory and revert when the plugin is disabled.
Open Obsidian → Open folder as vault → select your agent's skills directory.
For example:
~/OpenClaw/mojo/skills/ or ~/OpenClaw/harbs/skills/~/.claude/skills/Each skill subdirectory should contain a SKILL.md file with YAML frontmatter including a name field.
Clone or download this repository, then symlink it into your vault's plugins folder:
# Build the plugin first
cd <path-to-this-repo>
npm install && npm run build
# Symlink into your vault
mkdir -p <vault>/.obsidian/plugins
ln -s <path-to-this-repo> <vault>/.obsidian/plugins/obsidian-skill-graph
In Obsidian: Settings → Community plugins → Turn on community plugins → enable Agent Skill Graph
Press Cmd+P (or Ctrl+P) → search "Open graph view" → Enter. You should see skill nodes renamed and colored.
npm install
npm run dev # watch mode — rebuilds automatically on change
npm run build # production build
npm test # run unit tests
Settings → Community plugins → Agent Skill Graph (gear icon)
| Setting | Default | Description |
|---|---|---|
| Skill file name | SKILL.md |
Filename to scan |
| Skills folder | skills |
Folder whose direct .md children are also treated as skill nodes (empty to disable the folder rule) |
| Name field | name |
Frontmatter field used as the node label |
| Skill node color | #DE7356 |
Color for SKILL.md nodes |
| Agent node color | #7BAE6F |
Color for agent nodes (type: agent in frontmatter) |
| Reference node color | #5B8CA4 |
Color for referenced files inside the vault |
| External reference color | #DBDBDB |
Color for referenced files outside the vault |
Enter hex color codes (e.g. #ff6b6b). Changes take effect after reopening Graph View.
The plugin UI is localized in English and 正體中文 (Traditional Chinese), auto-selected from Obsidian's display language via the getLanguage() API.
If your skills are spread across multiple directories, you can use symlinks to consolidate them into a single Obsidian vault.
This is common when:
~/OpenClaw/agent-a/skills/, ~/OpenClaw/agent-b/skills/)~/.claude/skills/) and project-level skills (.claude/skills/ inside a repo)Create a dedicated directory and symlink each skill source:
mkdir ~/skill-vault
cd ~/skill-vault
# Example: OpenClaw agents
ln -s ~/OpenClaw/.openclaw/skills global
ln -s ~/OpenClaw/agent-a/skills agent-a
ln -s ~/OpenClaw/agent-b/skills agent-b
# Example: Claude Code
ln -s ~/.claude/skills claude-global
ln -s ~/my-project/.claude/skills my-project
Then open ~/skill-vault as an Obsidian vault.
Important: Obsidian ignores directories starting with
.(dotfiles). Use plain names likeglobalinstead of.openclawfor your symlink names.
Edits you make in this symlinked vault write straight back to the original skill files — there is no copy and no separate sync step.
This works because the plugin never writes to disk. It only reads skill files (vault.cachedRead) and reads external frontmatter (fs.readFileSync); all of its renaming, coloring, and edge work is in-memory Graph View state. Editing file content is therefore plain Obsidian behavior: because each skill directory is symlinked in, Obsidian writes through the symlink to the real source file, and the change takes effect there immediately.
Concretely:
name → the graph re-parses on the metadataCache change event and the node label updates live. The file itself keeps its name (e.g. SKILL.md); the plugin never renames files on disk.Exception: this applies only to files that are inside the vault (including the real files reached through your symlinks). It does not apply to out-of-vault external virtual nodes — those are read-only placeholders. Clicking one creates a blank note instead of opening the source (see Known Limitations).
The agent↔skill vault this plugin visualizes is best produced by skill-graph-builder — a framework-agnostic CLI/skill that scans your AI agent platforms (Claude Code, Codex, OpenClaw, Hermes) with zero configuration and emits a ready-to-open Obsidian vault.
It is the producer; this plugin is the consumer / viewer. The two form a producer→consumer workflow with no code coupling — each works independently, but they are designed to be used together. Both are MIT-licensed.
The generated vault uses the flat atomic model this plugin already understands:
<vault>/skills/<id>.md — one node per skill (a symlink to the real SKILL.md, deduplicated across sources and agents)<vault>/agents/<platform>-<profile>.md — one node per agent, linking to every skill it can accessA node is recognized as a skill node by either of two additive rules:
SKILL.md). This is the classic per-skill layout where each skill directory contains its own SKILL.md..md file located directly inside the configured Skills folder (default skills). This supports flat vaults such as the ones produced by skill-manager-sync, where every skill is a skills/<atomic-id>.md symlink and no file is literally named SKILL.md.The two rules are OR'd, so classic vaults keep working unchanged. Setting Skills folder to an empty string disables rule 2 (pure legacy exact-filename behavior). Detection is path/name based only (vault-relative parent path); no symlinks are resolved and no files are read from disk for this check.
Obsidian's Graph View renders with PixiJS (WebGL). The plugin listens to the layout-change event, scans renderer.nodes, and replaces the text._text property (a PixiJS Text object) of each SKILL.md node with the frontmatter name value. It also overrides getDisplayText() so other consumers receive the correct name.
The plugin injects SKILL.md → referenced-file entries into Obsidian's internal link tables, which Graph View reads to create PixiJS link objects:
metadataCache.resolvedLinks — drawn as solid edges to real file nodes.metadataCache.unresolvedLinks — drawn as edges to virtual nodes (files that exist on disk but not inside the vault).This is a pure in-memory operation — no files on disk are touched.
Node color is in { a: 1, rgb: 0xRRGGBB } format (PixiJS color). On every patch pass the plugin sets each node's color based on its type:
colorSkilltype: agent in frontmatter) → colorAgentcolorLocalRefcolorExternalRefA markdown file whose YAML frontmatter has type: agent is detected as an agent node and colored with colorAgent. This is intended for an "agent → visible skills" vault where each agent has one .md file containing markdown links to the skills it can see.
Agent → skill edges are produced by Obsidian's native markdown-link resolution — the plugin does not inject those edges (unlike SKILL.md reference edges). Detection uses metadataCache frontmatter only; if type: agent is later removed from a file, the plugin drops the now-stale node automatically.
layout-change fires frequently (resize, pane switch). Debouncing prevents redundant work._skillGraphPatched flag: already-patched nodes are skipped on subsequent passes.metadataCache.on('changed') re-parses only the changed SKILL.md.registerInterval timer re-runs the patch pass every 500 ms while the plugin is loaded, to catch nodes the renderer adds dynamically (e.g. graph animation mode) without a layout-change event. The _skillGraphPatched flag keeps each pass cheap, and registerInterval clears the timer automatically on unload.Reference path formats inside the OpenClaw ecosystem are inconsistent. The parser supports all of the following:
| Format | Example | Handling |
|---|---|---|
{baseDir}/ prefix |
`{baseDir}/scripts/run.sh` |
Strip prefix → scripts/run.sh |
| Backtick relative path | `references/SCHEMA.md` |
Extract directly |
| Markdown link | [FORMS.md](references/forms.md) |
Extract link target |
| CLI command path | python3 scripts/fetch.py |
Extract path after command |
| Absolute path | /home/user/.../scripts/fetch.py |
Strip vault prefix and convert |
Supported CLI keywords: python3, python, bash, node, sh
These never become graph nodes — they are discarded during parsing:
| Format | Where | Reason |
|---|---|---|
URLs (https://...) |
parse-references |
External links, not file references |
Strings without a / and a file extension |
parse-references |
Not a file-path shape (e.g. prose, bare words) |
Refs containing [ ] { } or YYYY |
skill-parser |
Templated/placeholder paths (e.g. reports/[market]/YYYY-MM.md) cannot resolve to a real file |
Path shapes that look like files but do not resolve to anything inside the vault are not dropped. They are injected into metadataCache.unresolvedLinks and shown as external virtual nodes (see Edges):
| Format | Example | Handling |
|---|---|---|
~-prefixed paths |
~/.openclaw/skills/foo/SKILL.md |
Shown as a virtual node; the plugin expands ~ and reads that file's frontmatter name for the label (see Privacy & Data Access) |
Dotfile paths (.openclaw/skills/…, .claude/skills/…) |
.openclaw/skills/foo/SKILL.md |
No special-casing — treated like any other unresolvable ref. Obsidian itself hides dotfile directories, so such a path never matches a vault file and becomes a virtual node |
| Any other relative path that no fallback strategy resolves | some/other/file.md |
Virtual node (no external name lookup unless ~-prefixed) |
After the parser extracts a relative path, skill-parser tries three strategies to locate the file inside the vault:
Strategy 1: relative to the SKILL.md's parent directory
scripts/run.sh → content-planner/scripts/run.sh
Works for: {baseDir} references and files inside the skill directory
Strategy 2: from vault root
content-analysis/SKILL.md → content-analysis/SKILL.md
Works for: sibling skill directories
Strategy 3: strip the first path segment
skills/content-analysis/SKILL.md → content-analysis/SKILL.md
Works for: workspace-relative paths when vault root equals skills/
Absolute paths are resolved by matching the vault basePath prefix:
/home/user/workspace/skills/my-skill/scripts/fetch.py
→ strip vault prefix
→ my-skill/scripts/fetch.py
Paths that cannot be resolved by any strategy are silently ignored (the file may be outside the vault).
src/
├── main.ts # Plugin entry point; event listeners, debounce, resolvedLinks injection
├── types.ts # SkillInfo, PixiJS Text, GraphNode/Renderer/View types
├── settings.ts # PluginSettingTab + defaults
├── skill-parser.ts # Scans vault SKILL.md files; parses frontmatter + reference paths
├── parse-references.ts # Pure function: extracts file paths from markdown text (unit-testable)
├── graph-patcher.ts # Hooks graph renderer; renames nodes + applies colors
└── lang/ # i18n: helpers.ts (getLanguage()-based locale pick) + locale/{en,zh-tw}.ts
vault file change
→ skill-parser re-parses SKILL.md
→ updates skillMap (Map<filePath, SkillInfo>)
→ main.ts injects resolvedLinks (so graph draws edges)
→ graph-patcher scans renderer.nodes
→ renames (text._text) + colors (node.color)
This plugin relies on the following undocumented Obsidian internals (confirmed via inspection):
| API | Purpose | Risk |
|---|---|---|
leaf.view.renderer.nodes |
Access graph node array | May change with Obsidian updates |
node.text._text |
PixiJS Text display string | May change with PixiJS version bumps |
node.color = { a, rgb } |
Node color (PixiJS format) | Same as above |
metadataCache.resolvedLinks |
Inject in-vault edges | Relatively stable; used by multiple plugins |
metadataCache.unresolvedLinks |
Inject out-of-vault (virtual node) edges | Relatively stable; used by multiple plugins |
renderer.colors.fillUnresolved |
Recolor external/virtual nodes (overrides Obsidian's default gray) | May change with Obsidian updates; saved and restored on unload |
renderer.renderCallback |
Wrapped to re-apply colors every frame (prevents flicker) | May change with Obsidian updates; original saved and unhooked on unload |
vault.adapter.basePath |
Get vault absolute path | Desktop only; not available on mobile |
This plugin accesses files outside the vault in one specific case: when a SKILL.md references an external file (e.g. ~/.openclaw/skills/foo/SKILL.md), the plugin reads that file's YAML frontmatter to extract the name field for display in the graph. Only the name field is used; no file content is modified. No data is sent over the network.
fs and vault.adapter.basePath; not compatible with mobile..external/ directory for out-of-vault files referenced by skills. This turns virtual nodes into real files that can be opened and browsed in Obsidian. Symlinks are cleaned up when the plugin is disabled. Desktop only (uses ln -s / mklink /J). Prior art: obsidian-symlink-plugin.description on hover.MIT