Summary
scan_skill_directory() uses globSync('**/SKILL.md', { cwd: dir }). Node's fs.globSync defaults to followSymlinks: false, so skill directories that are symlinks (or Windows junctions) are never entered and their SKILL.md is never found.
@spences10/pi-skills therefore reports 0 managed skills and the /skills UI shows 0 installed • 0 enabled / "No managed skills found", making enable/disable/delete unusable — while upstream pi loads those same skills without complaint.
Environment
- pi:
@earendil-works/pi-coding-agent 0.86.0 (vanilla pi, not the my-pi distribution)
@spences10/pi-skills 0.0.36
@spences10/pi-skill-importer 0.0.13
- Node v24.21.0, Windows 11 (also affects Linux/macOS — see below)
- Skill layout:
~/.pi/agent/skills/<name> are symlinks into a shared source directory (~/.pi/agent/skill-sources/...)
Steps to reproduce
Minimal, self-contained, no elevation required on Windows (uses a junction instead of a symlink):
import {
mkdtempSync, mkdirSync, writeFileSync, symlinkSync, globSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const root = mkdtempSync(join(tmpdir(), 'skills-repro-'));
const realSource = join(root, 'skill-sources', 'demo');
mkdirSync(realSource, { recursive: true });
writeFileSync(
join(realSource, 'SKILL.md'),
'---\nname: demo\ndescription: A demo skill used to reproduce the scanner bug.\n---\n\n# Demo\n',
);
// Common layout: ~/.pi/agent/skills/<name> links to a shared/cached source.
const skillsDir = join(root, 'skills');
mkdirSync(skillsDir, { recursive: true });
symlinkSync(
realSource,
join(skillsDir, 'demo'),
process.platform === 'win32' ? 'junction' : 'dir',
);
console.log('node', process.version);
console.log('globSync("**/SKILL.md", { cwd }) =>',
globSync('**/SKILL.md', { cwd: skillsDir }).length, 'match(es)');
console.log('globSync("**/SKILL.md", { cwd, followSymlinks: true }) =>',
globSync('**/SKILL.md', { cwd: skillsDir, followSymlinks: true }).length, 'match(es)');
Output:
node v24.21.0
globSync("**/SKILL.md", { cwd }) => 0 match(es)
globSync("**/SKILL.md", { cwd, followSymlinks: true }) => 1 match(es)
End-to-end through the package, with 6 real skills installed as symlinks in ~/.pi/agent/skills/:
getAgentDir() = C:\Users\<user>\.pi\agent # correct
scan_managed_skills() = 0
mgr.discover() = 0 installed, 0 enabled
get_enabled_skill_paths() = []
Expected
scan_managed_skills() returns the 6 skills, matching both packages/pi-skills/README.md ("discovers Pi-native skills in $PI_CODING_AGENT_DIR/skills") and upstream pi's own behaviour.
Why I believe this is unintended rather than out of scope
- No mention of symlink/junction/
followSymlinks in the source, docs, or tests of pi-skill-importer or pi-skills.
git log -S followSymlinks --all finds no commit ever adding it, so it was never deliberately removed.
- Upstream pi deliberately follows these links —
dist/core/skills.js (0.86.0) comments "For symlinks, check if they point to a directory and follow them". Splitting behaviour between loader and management UI is surprising: pi activates the skill, /skills says it does not exist.
Happy to be told this is deliberate — in that case the package READMEs should state that symlinked skill directories are excluded.
Root cause
packages/pi-skill-importer/src/scanner-primitives.ts:71
const matches = globSync('**/SKILL.md', { cwd: dir });
Per the Node docs, followSymlinks — "When true, symbolic links to directories are followed while expanding ** patterns" — defaults to false and was only added in Node v24.16.0.
Suggested fix
-const matches = globSync('**/SKILL.md', { cwd: dir });
+const matches = globSync('**/SKILL.md', {
+ cwd: dir,
+ followSymlinks: true,
+});
Two things to decide:
- Node floor.
followSymlinks requires Node >= 24.16.0, but both packages declare "engines": { "node": ">=24.15.0" }. Either bump the floor to >=24.16.0, or feature-detect / walk manually (below).
- Scope. The neighbouring
globSync('*.md', { cwd: dir }) for root Markdown skills appears unaffected, because *.md matches the link itself rather than descending into it. Only the ** descent needs the option. FWIW the project-skill scans share this helper, so symlinked .agents/skills/<name> entries are affected too.
A more compatible alternative is to stop using globSync for descent and follow symlinks explicitly with statSync, mirroring upstream pi's loadSkillsFromDirInternal — same behaviour on every supported Node.
Impact
Any user whose skills are symlinked or junctioned — a normal layout for dotfiles repos, shared skill caches, or ~/.claude/skills linked into ~/.pi/agent/skills — sees "0 installed" and cannot manage skills at all, even though pi loads and uses them.
Locally patched with the one-line followSymlinks: true change and verified: scan_managed_skills() 0 -> 6. I'm carrying that patch manually until this is fixed upstream.
Summary
scan_skill_directory()usesglobSync('**/SKILL.md', { cwd: dir }). Node'sfs.globSyncdefaults tofollowSymlinks: false, so skill directories that are symlinks (or Windows junctions) are never entered and theirSKILL.mdis never found.@spences10/pi-skillstherefore reports 0 managed skills and the/skillsUI shows0 installed • 0 enabled/ "No managed skills found", making enable/disable/delete unusable — while upstream pi loads those same skills without complaint.Environment
@earendil-works/pi-coding-agent0.86.0 (vanillapi, not themy-pidistribution)@spences10/pi-skills0.0.36@spences10/pi-skill-importer0.0.13~/.pi/agent/skills/<name>are symlinks into a shared source directory (~/.pi/agent/skill-sources/...)Steps to reproduce
Minimal, self-contained, no elevation required on Windows (uses a junction instead of a symlink):
Output:
End-to-end through the package, with 6 real skills installed as symlinks in
~/.pi/agent/skills/:Expected
scan_managed_skills()returns the 6 skills, matching bothpackages/pi-skills/README.md("discovers Pi-native skills in$PI_CODING_AGENT_DIR/skills") and upstream pi's own behaviour.Why I believe this is unintended rather than out of scope
followSymlinksin the source, docs, or tests ofpi-skill-importerorpi-skills.git log -S followSymlinks --allfinds no commit ever adding it, so it was never deliberately removed.dist/core/skills.js(0.86.0) comments "For symlinks, check if they point to a directory and follow them". Splitting behaviour between loader and management UI is surprising: pi activates the skill,/skillssays it does not exist.Happy to be told this is deliberate — in that case the package READMEs should state that symlinked skill directories are excluded.
Root cause
packages/pi-skill-importer/src/scanner-primitives.ts:71Per the Node docs,
followSymlinks— "Whentrue, symbolic links to directories are followed while expanding**patterns" — defaults tofalseand was only added in Node v24.16.0.Suggested fix
Two things to decide:
followSymlinksrequires Node >= 24.16.0, but both packages declare"engines": { "node": ">=24.15.0" }. Either bump the floor to>=24.16.0, or feature-detect / walk manually (below).globSync('*.md', { cwd: dir })for root Markdown skills appears unaffected, because*.mdmatches the link itself rather than descending into it. Only the**descent needs the option. FWIW the project-skill scans share this helper, so symlinked.agents/skills/<name>entries are affected too.A more compatible alternative is to stop using
globSyncfor descent and follow symlinks explicitly withstatSync, mirroring upstream pi'sloadSkillsFromDirInternal— same behaviour on every supported Node.Impact
Any user whose skills are symlinked or junctioned — a normal layout for dotfiles repos, shared skill caches, or
~/.claude/skillslinked into~/.pi/agent/skills— sees "0 installed" and cannot manage skills at all, even though pi loads and uses them.Locally patched with the one-line
followSymlinks: truechange and verified:scan_managed_skills()0 -> 6. I'm carrying that patch manually until this is fixed upstream.