Skill System
skills.ts implements the Agent Skills spec (see https://agentskills.io/integrate-skills). A skill is a markdown file with frontmatter describing its purpose. After the LLM sees the skill list in the system prompt, it reads the corresponding SKILL.md via the read tool to get detailed instructions. This file handles skill discovery, loading, validation, deduplication, and formatting as a prompt section.
Responsibilities
- Discovery rules:
loadSkillsFromDirInternalrecursively scans a directory — encounteringSKILL.mdis treated as a skill root and recursion stops; otherwise it scans subdirectories and root-level.mdfiles. Seepackages/coding-agent/src/core/skills.ts:173-280. - Frontmatter validation:
validateNamechecks the name (a-z0-9-, max 64 chars, no leading/trailing hyphen, no--, matches the parent directory name);validateDescriptionchecks the description is required and max 1024 chars. Seepackages/coding-agent/src/core/skills.ts:93-117andpackages/coding-agent/src/core/skills.ts:122-132. - Multi-source loading:
loadSkillsloads from the user global (~/.pi/agent/skills), the project (./.pi/skills), and explicitskillPaths. On collision it keeps the first registered and emits acollisiondiagnostic. Seepackages/coding-agent/src/core/skills.ts:405-504. - Deduplication: uses
realpathto resolve symlinks, so the same file registered via different paths only counts once; same-named skills are first-come-first-served. Seepackages/coding-agent/src/core/skills.ts:416-445. - Prompt formatting:
formatSkillsForPromptgenerates an<available_skills>XML block, with each skill as a<name>/<description>/<location>triple. Skills withdisableModelInvocation: trueare excluded from the prompt (they can only be invoked explicitly via/skill:name). Seepackages/coding-agent/src/core/skills.ts:340-366. - Ignore rules: respects
.gitignore/.ignore/.fdignore, with directory-prefixed path patterns during recursive scans. Seepackages/coding-agent/src/core/skills.ts:48-66andpackages/coding-agent/src/core/skills.ts:25-46.
Design Motivation
Why use SKILL.md instead of scanning all .md files? A skill may contain multiple files (scripts, sub-docs); SKILL.md is the entry marker. Once encountered, recursion stops and the whole directory is treated as a skill package, letting authors organize multiple files without them being picked up as separate skills.
Why frontmatter instead of comments? Frontmatter is YAML — structured, parseable, validatable. Fields like name, description, disable-model-invocation need to be readable by both the LLM and programs. validateName enforces that the name matches the parent directory name (name "x" does not match parent directory "y"), so skills don't drift after being moved.
Why disableModelInvocation? Some skills are user-private prompt templates that shouldn't be auto-triggered by the LLM (e.g., "use this style when writing code"); they should only be loaded when the user explicitly runs /skill:name. formatSkillsForPrompt filters these out, so only skills allowed for LLM auto-invocation are exposed in the prompt.
Why dedupe via realpath? A user might pass --skills ./my-skills while also symlinking ./my-skills to ~/.pi/agent/skills/my-skills. Two scans would pick up the same file via different paths. canonicalizePath resolves symlinks before comparing, avoiding double-loading.
Key Files
packages/coding-agent/src/core/skills.ts:11-19— constants:MAX_NAME_LENGTH64,MAX_DESCRIPTION_LENGTH1024,IGNORE_FILE_NAMES.packages/coding-agent/src/core/skills.ts:21-46—toPosixPathandprefixIgnorePattern, prefixing subdirectory ignore rules with the directory.packages/coding-agent/src/core/skills.ts:75-87—Skillinterface:name,description,filePath,baseDir,sourceInfo,disableModelInvocation.packages/coding-agent/src/core/skills.ts:93-117—validateName, five rules.packages/coding-agent/src/core/skills.ts:173-280—loadSkillsFromDirInternal, recursive scan and ignore filtering.packages/coding-agent/src/core/skills.ts:282-330—loadSkillFromFile, frontmatter parsing and validation.packages/coding-agent/src/core/skills.ts:340-366—formatSkillsForPrompt, XML block generation.packages/coding-agent/src/core/skills.ts:405-504—loadSkills, multi-source loading and dedup.
formatSkillsForPrompt wraps the skill list in XML tags and escapes XML special characters:
// packages/coding-agent/src/core/skills.ts:340-366
export function formatSkillsForPrompt(skills: Skill[]): string {
const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
if (visibleSkills.length === 0) {
return "";
}
const lines = [
"\n\nThe following skills provide specialized instructions for specific tasks.",
"Use the read tool to load a skill's file when the task matches its description.",
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
"",
"<available_skills>",
];
for (const skill of visibleSkills) {
lines.push(" <skill>");
lines.push(` <name>${escapeXml(skill.name)}</name>`);
lines.push(` <description>${escapeXml(skill.description)}</description>`);
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
lines.push(" </skill>");
}
lines.push("</available_skills>");
return lines.join("\n");
}loadSkillsFromDirInternal returns immediately upon encountering SKILL.md and does not recurse further:
// packages/coding-agent/src/core/skills.ts:199-226
for (const entry of entries) {
if (entry.name !== "SKILL.md") {
continue;
}
const fullPath = join(dir, entry.name);
let isFile = entry.isFile();
if (entry.isSymbolicLink()) {
try {
isFile = statSync(fullPath).isFile();
} catch {
continue;
}
}
const relPath = toPosixPath(relative(root, fullPath));
if (!isFile || ig.ignores(relPath)) {
continue;
}
const result = loadSkillFromFile(fullPath, source);
if (result.skill) {
skills.push(result.skill);
}
diagnostics.push(...result.diagnostics);
return { skills, diagnostics };
}Data Flow
Skills from disk to prompt:
Edges and Failures
- Refuse to load without description: when
frontmatter.descriptionis empty or trims to empty, it returns{ skill: null }and never enters the skillMap. Seepackages/coding-agent/src/core/skills.ts:310-312. - Load even when name is invalid: name validation only produces a warning diagnostic and doesn't block loading, so users can see the problem while the skill still works. See
packages/coding-agent/src/core/skills.ts:300-307. - Broken symlinks: when
statSyncthrows,continueskips it without aborting the whole scan. Seepackages/coding-agent/src/core/skills.ts:207-213andpackages/coding-agent/src/core/skills.ts:241-252. - Skip node_modules: recursion explicitly skips
node_modulesto avoid scanning.mdfiles in dependencies. Seepackages/coding-agent/src/core/skills.ts:233-236. - Path type resolution: an explicit
skillPathsentry can be a file or directory. Directories go throughloadSkillsFromDirInternal; files must be.md. Seepackages/coding-agent/src/core/skills.ts:478-498. - Source identification: even with
includeDefaults: false, explicit paths are still classified as user/project sources based on path prefix; otherwise marked aspath. Seepackages/coding-agent/src/core/skills.ts:464-470.
Summary
skills.ts implements the Agent Skills spec: three-source loading (user global, project, explicit path), frontmatter validation, realpath dedup, and XML formatting injected into the prompt. Once a skill is referenced by the system prompt, it's read via the read tool — see System Prompt Construction and Tools: read/bash/edit/write/grep/find/ls. For the entry point that wires up skill paths, see CLI Entry and Dispatch.