This page covers the mechanics of how skills are found on disk, how their metadata is parsed, and how a skill name is resolved to a file path when multiple copies exist at different priority levels. This is an implementation-level document focused on lib/skills-core.js.
For the conceptual priority hierarchy (project > personal > superpowers) and how agents are instructed to use it, see Skill Priority and Overriding. For how the skills-core.js module fits into the broader shared library, see skills-core.js Shared Module.
Skills are stored as directories, each containing a SKILL.md file. The discovery and resolution system must:
name and descriptionAll of this logic lives in lib/skills-core.js which exports five functions: extractFrontmatter, findSkillsInDir, resolveSkillPath, checkForUpdates, and stripFrontmatter.
Sources: lib/skills-core.js1-10
Each skill is a directory. The only required file inside it is SKILL.md. The file starts with a YAML frontmatter block delimited by --- lines, followed by the Markdown body.
skills/
brainstorming/
SKILL.md ← frontmatter + content
test-driven-development/
SKILL.md
using-superpowers/
SKILL.md
nested-group/
some-skill/
SKILL.md ← nested discovery supported up to maxDepth=3
A minimal SKILL.md:
Sources: lib/skills-core.js6-15 tests/opencode/test-skills-core.sh22-31
Skills can reside in three separate root directories, each corresponding to a different scope and priority level.
| Priority | Scope | Typical Path |
|---|---|---|
| 1 (highest) | Project | .opencode/skills/ in the working directory |
| 2 | Personal | ~/.config/opencode/skills/ |
| 3 (lowest) | Superpowers library | ~/.config/opencode/skills/superpowers/ (symlink to repo skills/) |
The superpowers skills directory is not a separate root scanned by resolveSkillPath; it is a subdirectory under the personal skills root because it is symlinked there during installation. resolveSkillPath receives explicit superpowersDir and personalDir arguments rather than discovering roots dynamically.
Sources: docs/README.opencode.md232-237 lib/skills-core.js99-140
extractFrontmatterSignature: extractFrontmatter(filePath) → { name, description }
Reads a SKILL.md file and parses the YAML frontmatter block. The parser is intentionally minimal: it does not use a full YAML library. It scans line-by-line, entering frontmatter mode on the first --- and exiting on the second. It recognises only name and description keys.
Frontmatter parsing rules:
| Rule | Detail |
|---|---|
| Block delimiters | Lines containing only --- (after .trim()) |
| Key extraction | Regex ^(\w+):\s*(.*)$ — only single-word keys are matched |
| Supported keys | name, description |
| Unknown keys | Silently ignored |
| File read errors | Returns { name: '', description: '' } |
If name is absent from the frontmatter, the caller (findSkillsInDir) falls back to using the directory name as the skill name.
Frontmatter parsing state machine:
Sources: lib/skills-core.js16-52 tests/opencode/test-skills-core.sh34-84
findSkillsInDirSignature: findSkillsInDir(dir, sourceType, maxDepth = 3) → Array<SkillEntry>
Recursively walks a root directory. For each subdirectory encountered, it checks whether a SKILL.md file exists inside that directory. If it does, the skill is added to the result list and extractFrontmatter is called to populate name and description. The function also recurses into the subdirectory regardless of whether it contained a SKILL.md, allowing nested groupings.
Return type per skill entry:
| Field | Type | Value |
|---|---|---|
path | string | Absolute path to the skill directory |
skillFile | string | Absolute path to SKILL.md |
name | string | From frontmatter, or directory name if absent |
description | string | From frontmatter, or '' |
sourceType | string | The value passed in as sourceType argument |
Recursion behaviour:
dir does not exist (fs.existsSync check)entry.isDirectory() is truemaxDepth (default: 3)SKILL.md still get recursed into (enabling subdirectory grouping)Directory scan flow:
Sources: lib/skills-core.js54-97 tests/opencode/test-skills-core.sh134-246
resolveSkillPathSignature: resolveSkillPath(skillName, superpowersDir, personalDir) → SkillRef | null
Given a skill name string, returns the file path of the highest-priority matching skill, or null if no match is found. This function implements the priority shadowing logic.
Return type:
| Field | Type | Value |
|---|---|---|
skillFile | string | Absolute path to SKILL.md |
sourceType | string | 'personal' or 'superpowers' |
skillPath | string | Normalised skill name (prefix stripped) |
Resolution algorithm:
Priority override with superpowers: prefix:
The superpowers: prefix is an escape hatch that bypasses personal skill shadowing. When skillName is "superpowers:brainstorming", the function strips the prefix to get "brainstorming" and skips the personal directory check entirely.
Input skillName | personalDir has match | Result |
|---|---|---|
"brainstorming" | yes | personal version returned |
"brainstorming" | no | superpowers version returned |
"superpowers:brainstorming" | yes | superpowers version returned (prefix forces it) |
"brainstorming" | no match in either | null |
Sources: lib/skills-core.js99-140 tests/opencode/test-skills-core.sh248-363 tests/opencode/test-priority.sh152-175
stripFrontmatterSignature: stripFrontmatter(content) → string
Takes the full string content of a SKILL.md file and returns only the Markdown body, with the frontmatter block removed. Lines between the two --- delimiters are discarded. The result is trimmed.
Used by callers that need to inject skill content into a prompt without exposing the raw YAML metadata to the agent.
Sources: lib/skills-core.js178-200 tests/opencode/test-skills-core.sh87-132
checkForUpdatesSignature: checkForUpdates(repoDir) → boolean
Runs git fetch origin && git status --porcelain=v1 --branch in the given directory with a 3-second timeout. Parses the output to check whether the local branch is behind the remote (<FileRef file-url="https://github.com/obra/superpowers/blob/e4a2375c/behind in the branch status line). Returns false on any error (network down, not a git repo, timeout).\n\nThis is used at session bootstrap time to alert the user when a newer version of the skills library is available. The short timeout ensures it does not block the session start.\n\nSources#LNaN-LNaN" NaN file-path="behind in the branch status line). Returnsfalse` on any error (network down, not a git repo, timeout).\n\nThis is used at session bootstrap time to alert the user when a newer version of the skills library is available. The short timeout ensures it does not block the session start.\n\nSources">Hii tests/opencode/test-skills-core.sh366-436
The module uses ES module syntax (import/export). It has no external npm dependencies — only Node.js built-ins fs, path, and child_process.
Sources: lib/skills-core.js1-4 lib/skills-core.js202-208
The following shows how a request to load "brainstorming" flows through the system when a personal override exists:
If the caller prefixes the name with superpowers:, resolveSkillPath skips the existsSync call against personalDir entirely and goes directly to superpowersDir.
Sources: lib/skills-core.js108-139 lib/skills-core.js178-200
The test suite for this module is in tests/opencode/test-skills-core.sh It tests each exported function in isolation by inlining the JavaScript rather than importing the ES module, avoiding ESM resolution issues in the test environment.
| Test | Function | What it verifies |
|---|---|---|
| Test 1 | extractFrontmatter | name and description fields parsed from SKILL.md |
| Test 2 | stripFrontmatter | Body preserved, frontmatter removed |
| Test 3 | findSkillsInDir | Flat and nested skills both discovered |
| Test 4 | resolveSkillPath | Personal shadows superpowers; superpowers: prefix overrides; missing skill returns null |
| Test 5 | checkForUpdates | Graceful false return for no-remote, non-existent, and non-git directories |
Priority resolution under real OpenCode conditions is tested separately in tests/opencode/test-priority.sh
Sources: tests/opencode/test-skills-core.sh1-441 tests/opencode/run-tests.sh60-65
Refresh this wiki
This wiki was recently refreshed. Please wait 3 days to refresh again.