Instead of injecting the full SKILL.md body as additionalContext, inject "You must run the Skill(<name>) tool." — a more conventional way of telling the agent to use the Skill tool for context loading.
34 KiB
Skill Authoring & Frontmatter Reference
Audience: Skill authors — anyone adding new skills or extending existing ones.
This guide walks you through creating a new skill from scratch, explains every frontmatter field, documents the scoring engine, the validation system, the manifest build pipeline, the template include engine, and the custom YAML parser's non-standard behavior. It includes annotated real-world examples from the skills/ directory.
Table of Contents
- User Story: Adding a New Skill End-to-End
- SKILL.md Frontmatter Schema
- Annotated Real Skill Examples
- Pattern Matching Reference
- Prompt Signal Scoring
- Validation Rules
- Manifest Build Pipeline
- Template Include Engine
- Custom YAML Parser Gotchas
- Build & Test Workflow
User Story: Adding a New Skill End-to-End
Scenario: You're a developer on the Vercel plugin team. Vercel just shipped a new feature — say, "Edge Config" — and you want Claude to automatically inject best-practice guidance whenever a developer touches Edge Config files, runs related commands, or asks about it in a prompt.
The journey
flowchart LR
A["1. Create directory<br/>skills/edge-config/"] --> B["2. Write SKILL.md<br/>frontmatter + body"]
B --> C["3. Build manifest<br/>bun run build:manifest"]
C --> D["4. Validate<br/>bun run validate"]
D --> E["5. Test with explain CLI<br/>bun run explain -- --file edge-config.json"]
E --> F["6. Run tests<br/>bun test"]
F --> G["7. Build all & commit<br/>bun run build"]
Step 1 — Create the skill directory
mkdir -p skills/edge-config
Skills are keyed by directory name, not the frontmatter name field. The directory name is the canonical identifier used everywhere: dedup, manifest, env vars, and logs.
Step 2 — Write SKILL.md
Create skills/edge-config/SKILL.md with YAML frontmatter between --- delimiters, followed by the guidance body in markdown. See the Frontmatter Schema section for every available field.
Minimal skeleton:
---
name: edge-config
description: "Best practices for Vercel Edge Config — a low-latency global data store"
summary: "Edge Config: use read() not get(), prefer JSON values"
metadata:
priority: 6
pathPatterns:
- "edge-config.*"
bashPatterns:
- "\\bedge.config\\b"
importPatterns:
- "@vercel/edge-config"
promptSignals:
phrases:
- "edge config"
minScore: 6
validate:
- pattern: "edgeConfig\\.get\\("
message: "Use edgeConfig.read() instead of .get() — read() returns typed values"
severity: error
---
# Edge Config
You are an expert in Vercel Edge Config...
Step 3 — Build the manifest
bun run build:manifest
This reads all skills/*/SKILL.md, extracts frontmatter, compiles glob→regex and import→regex at build time, and writes generated/skill-manifest.json. Hooks read the manifest at runtime for fast matching — they never parse SKILL.md live.
Step 4 — Validate
bun run validate
Checks that frontmatter parses, required fields are present, patterns are valid (globs compile, regexes parse), and the manifest is in sync.
Step 5 — Test with the explain CLI
# File path matching
bun run scripts/explain.ts --file edge-config.json
# Bash command matching
bun run scripts/explain.ts --bash "vercel edge-config ls"
# With profiler boost simulation
bun run scripts/explain.ts --file edge-config.json --likely-skills edge-config
The explain command mirrors runtime logic exactly — it shows priority scores, match reasons, and whether your skill would be injected within the budget.
Step 6 — Run the full test suite
bun test
Step 7 — Build everything and commit
bun run build # hooks + manifest + from-skills
bun test # final verification
What happens at runtime after you ship
- SessionStart — The profiler scans
package.json. If@vercel/edge-configis a dependency, your skill gets a +5 priority boost viaVERCEL_PLUGIN_LIKELY_SKILLS. - PreToolUse — When Claude reads
edge-config.jsonor runsvercel env pull, yourpathPatternsandbashPatternsmatch → the skill is ranked, deduped, and injected within the 18KB budget (max 3 skills). - UserPromptSubmit — When the developer types "how do I set up edge config?", your
promptSignals.phrasesscore +6 → the skill injects within the 8KB budget (max 2 skills). - PostToolUse — When Claude writes to a matched file, your
validaterules run and flag antipatterns.
User Story: TSX Edit Trigger (react-best-practices)
Scenario: You're a developer building a React dashboard. You've been editing
.tsxcomponents for a while — adding hooks, state, and effects. After your 3rd.tsxedit, Claude suddenly has React best-practices guidance it didn't have before.
What's happening under the hood
The PreToolUse hook tracks .tsx edits via VERCEL_PLUGIN_TSX_EDIT_COUNT. When the count hits the threshold (default 3, configurable via VERCEL_PLUGIN_REVIEW_THRESHOLD), the react-best-practices skill injects with a +40 priority boost.
sequenceDiagram
participant Dev as Developer
participant CC as Claude Code
participant Hook as PreToolUse
Dev->>CC: Edit Button.tsx (add onClick handler)
CC->>Hook: Edit tool on Button.tsx
Note over Hook: TSX count: 0 → 1
Dev->>CC: Edit Dashboard.tsx (add useEffect)
CC->>Hook: Edit tool on Dashboard.tsx
Note over Hook: TSX count: 1 → 2
Dev->>CC: Edit Sidebar.tsx (add useState)
CC->>Hook: Edit tool on Sidebar.tsx
Note over Hook: TSX count: 2 → 3 ← threshold!
Note over Hook: Inject react-best-practices (+40 boost)
Hook-->>CC: React guidance injected
Note over CC: Claude now flags missing<br/>"use client", suggests memoization,<br/>checks hook dependencies
Dev->>CC: Edit Header.tsx
CC->>Hook: Edit tool on Header.tsx
Note over Hook: TSX count: 0 → 1 (reset after injection)
How to configure this in your skill
The TSX review trigger is hard-coded to the react-best-practices skill — you can't create custom edit-count triggers for other skills. But understanding the pattern helps you design skills that complement it: if your skill is about a React library (e.g., swr, shadcn), its patterns will fire alongside react-best-practices when matching files are edited.
User Story: Dev Server Detection (agent-browser-verify)
Scenario: You've just finished building a login form and ask Claude to start the dev server. Claude not only starts
npm run dev, but also opens a browser to visually verify the form renders correctly.
What's happening under the hood
The PreToolUse hook detects dev server commands (next dev, npm run dev, pnpm dev, bun dev, vite, etc.) and injects agent-browser-verify with a +45 priority boost — but only if the agent-browser CLI is installed.
flowchart TD
BASH["Bash tool: npm run dev"] --> DETECT{"Matches dev<br/>server pattern?"}
DETECT -->|No| NORMAL["Normal pattern matching"]
DETECT -->|Yes| AVAIL{"agent-browser<br/>CLI installed?"}
AVAIL -->|No| NOTICE["Inject unavailability notice<br/>(suggest installation)"]
AVAIL -->|Yes| GUARD{"Loop guard:<br/>count < 2?"}
GUARD -->|No| SKIP["Skip (prevent infinite loops)"]
GUARD -->|Yes| INJECT["Inject agent-browser-verify (+45)<br/>+ verification companion (summary)"]
Key design details
- The session-start profiler sets
VERCEL_PLUGIN_AGENT_BROWSER_AVAILABLE=1if the CLI is found on PATH - A loop guard caps injection at 2 per session (
VERCEL_PLUGIN_DEV_VERIFY_COUNT) — repeatednpm run devrestarts won't flood context - The
verificationskill is co-injected as a summary-only companion to provide a verification checklist without consuming too much budget
User Story: Prompt Signal Matching (UserPromptSubmit)
Scenario: A developer types "how do I add a durable workflow that survives crashes?" into Claude Code. Without touching any files, Claude gets the
workflowskill injected because the prompt signals match.
What's happening under the hood
The UserPromptSubmit hook normalizes the prompt and scores it against every skill's promptSignals:
Normalized prompt: "how do i add a durable workflow that survive crash"
(lowercase → contraction expansion → stemming: "survives" → "survive", "crashes" → "crash")
Scoring for workflow skill:
# workflow's promptSignals:
phrases: ["durable workflow"] # substring match → +6
allOf: [["workflow", "durable"]] # both present → +4
anyOf: ["durable", "reliable"] # "durable" +1 → +1
minScore: 4
| Component | Match | Score |
|---|---|---|
phrase: "durable workflow" |
Yes | +6 |
allOf: ["workflow", "durable"] |
Both present | +4 |
anyOf: "durable" |
Yes | +1 |
| Total | 11 |
11 >= minScore 4 → skill injects within the 8KB / 2-skill budget.
How to write effective prompt signals for your skill
-
Start with phrases — these are the strongest signals (+6 each). Use the exact phrases your users type:
"edge config","ai sdk","deploy to vercel". -
Add allOf groups for concepts — combinations of terms that together indicate intent:
[["cron", "schedule"]],[["streaming", "response"]]. Each fully-matched group scores +4. -
Sprinkle anyOf for weak signals — individual terms that slightly boost relevance:
"timeout","cache","optimize". Capped at +2 total to prevent noise. -
Use noneOf to suppress false positives — terms that indicate the user is asking about something else:
["github actions", ".github/workflows"]in the workflow skill prevents matching GitHub CI prompts. -
Set minScore appropriately — default 6 (one phrase match). Lower to 4 for broad skills like
investigation-modethat should match on weaker signals.
SKILL.md Frontmatter Schema
Every SKILL.md begins with a YAML frontmatter block between --- delimiters. Below is the complete schema with types, defaults, and descriptions.
Top-Level Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string |
No | directory name | Human-readable name. Falls back to the directory name if omitted. |
description |
string |
Yes | — | One-line description of the skill's purpose. Used in manifest, logs, and lexical fallback scoring. |
summary |
string |
Recommended | — | Brief fallback text injected when the full body exceeds the byte budget. Keep under ~200 chars. |
metadata |
object |
Yes | — | Contains all matching, scoring, and validation configuration. |
validate |
object[] |
No | [] |
PostToolUse validation rules (can also live inside metadata). |
retrieval |
object |
No | — | Discovery metadata for search/retrieval systems. |
metadata Object
| Field | Type | Default | Description |
|---|---|---|---|
priority |
number |
5 |
Injection priority (range 4–8). Higher = injected first when multiple skills match. |
pathPatterns |
string[] |
[] |
Glob patterns for file path matching. Compiled to regex at build time. |
bashPatterns |
string[] |
[] |
JavaScript regex patterns for bash command matching. |
importPatterns |
string[] |
[] |
Package name patterns for import/require matching. Supports * for scoped wildcards. |
promptSignals |
object |
— | Prompt-based scoring configuration (see below). |
promptSignals Object
Controls how the UserPromptSubmit hook scores user prompts against this skill.
| Field | Type | Default | Description |
|---|---|---|---|
phrases |
string[] |
[] |
Exact substring matches (case-insensitive). Each hit scores +6. |
allOf |
string[][] |
[] |
Groups of terms that must all appear in the prompt. Each fully-matched group scores +4. |
anyOf |
string[] |
[] |
Optional terms. Each hit scores +1, capped at +2 total. |
noneOf |
string[] |
[] |
Suppression terms. Any match sets the score to -Infinity (hard suppress — skill never injects). |
minScore |
number |
6 |
Minimum total score required for the skill to be injected. |
validate Array
Each entry defines a PostToolUse validation rule that runs when Claude writes or edits a file matched by the skill's path patterns.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
pattern |
string |
Yes | — | Regex pattern to search for in the written file content. |
message |
string |
Yes | — | Error/warning message shown to Claude when pattern matches. Should be actionable (tell Claude what to do, not just what's wrong). |
severity |
"error" or "warn" |
Yes | — | error = Claude must fix before proceeding. warn = advisory. |
skipIfFileContains |
string |
No | — | Regex — if the file also matches this pattern, skip the rule entirely. Prevents false positives. |
retrieval Object
Optional metadata for discovery and search systems.
| Field | Type | Description |
|---|---|---|
aliases |
string[] |
Alternative names users might search for (e.g., ["vercel ai", "ai library"]). |
intents |
string[] |
User intents this skill addresses (e.g., ["add ai to app", "set up streaming"]). |
entities |
string[] |
Key API symbols, functions, or types (e.g., ["useChat", "streamText"]). |
Annotated Real Skill Examples
Example 1: nextjs
File: skills/nextjs/SKILL.md — a complex skill with prompt signals, 11 validation rules, and broad pattern coverage.
---
name: nextjs
description: Next.js App Router expert guidance. Use when building, debugging,
or architecting Next.js applications — routing, Server Components, Server
Actions, Cache Components, layouts, middleware/proxy, data fetching,
rendering strategies, and deployment on Vercel.
metadata:
priority: 5 # ← Default priority; not boosted
# because Next.js is so common the
# profiler adds +5 when detected.
pathPatterns:
- 'next.config.*' # ← Matches next.config.js, .mjs, .ts
- 'next-env.d.ts'
- 'app/**' # ← Catches all App Router files
- 'pages/**' # ← Also catches Pages Router (for migration guidance)
- 'src/app/**' # ← Common src/ layout variant
- 'src/pages/**'
- 'tailwind.config.*' # ← Tailwind is tightly coupled with Next.js projects
- 'postcss.config.*'
- 'tsconfig.json'
- 'tsconfig.*.json'
- 'apps/*/app/**' # ← Monorepo patterns (Turborepo convention)
- 'apps/*/pages/**'
- 'apps/*/src/app/**'
- 'apps/*/src/pages/**'
- 'apps/*/next.config.*'
bashPatterns:
- '\bnext\s+(dev|build|start|lint)\b' # ← Core Next.js CLI commands
- '\bnext\s+experimental-analyze\b' # ← Bundle analyzer
- '\bnpx\s+create-next-app\b' # ← Project scaffolding
- '\bbunx\s+create-next-app\b'
- '\bnpm\s+run\s+(dev|build|start)\b' # ← npm scripts that likely invoke Next.js
- '\bpnpm\s+(dev|build)\b'
- '\bbun\s+run\s+(dev|build)\b'
promptSignals:
phrases: # ← Each phrase hit = +6
- "next.js"
- "nextjs"
- "app router"
- "server component"
- "server action"
allOf: # ← All terms in group must match = +4
- [middleware, next] # "next middleware" → +4
- [layout, route] # "route layout" → +4
anyOf: # ← Each hit = +1, capped at +2
- "pages router"
- "getserversideprops"
- "use server"
noneOf: [] # ← No suppression terms
minScore: 6 # ← One phrase match is enough
validate: # ← 11 rules catching common mistakes
- pattern: export.*getServerSideProps
message: 'getServerSideProps is removed in App Router — use server
components or route handlers'
severity: error # ← error = Claude must fix
- pattern: (useState|useEffect)
message: 'React hooks require "use client" directive — add it at the
top of client components'
severity: warn
skipIfFileContains: "^['\"]use client['\"]"
# ↑ Skip if file already has "use client"
- pattern: useRef\(\s*\)
message: 'useRef() requires an initial value in React 19 — use useRef(null)'
severity: error
- pattern: (?<!await )\bcookies\(\s*\)
message: 'cookies() is async in Next.js 16 — add await'
severity: error
skipIfFileContains: "^['\"]use client['\"]"
# ↑ Client components don't call cookies()
---
# Next.js (App Router)
...guidance body follows...
What makes this skill effective:
- pathPatterns cover both standard and monorepo layouts, catching files whether they're in
app/,src/app/, orapps/*/app/. - promptSignals use all four scoring mechanisms:
phrasesfor strong direct matches,allOffor multi-term concepts,anyOffor weaker signals, andminScore: 6so a single phrase is sufficient. - validate rules target real migration pitfalls (Pages Router → App Router, React 18 → 19, Next.js 15 → 16) and use
skipIfFileContainsto avoid false positives on client components.
Example 2: email
File: skills/email/SKILL.md — a simpler skill with only path/bash patterns, no prompt signals or validation rules.
---
name: email
description: Email sending integration guidance — Resend (native Vercel
Marketplace) with React Email templates. Covers API setup, transactional
emails, domain verification, and template patterns.
metadata:
priority: 4 # ← Lower priority; email is a secondary
# concern, not the primary framework.
pathPatterns:
- 'emails/**' # ← Email template directories
- 'src/emails/**'
- 'components/emails/**'
- 'src/components/emails/**'
- 'app/api/send/**' # ← API routes for sending
- 'src/app/api/send/**'
- 'app/api/email/**'
- 'src/app/api/email/**'
- 'app/api/emails/**'
- 'src/app/api/emails/**'
- 'lib/resend.*' # ← Resend client setup files
- 'src/lib/resend.*'
- 'lib/email.*'
- 'src/lib/email.*'
- 'lib/email*'
- 'src/lib/email*'
- '**/email-template*'
bashPatterns: # ← Cover all 4 package managers × 3 packages
- '\bnpm\s+(install|i|add)\s+[^\n]*\bresend\b'
- '\bpnpm\s+(install|i|add)\s+[^\n]*\bresend\b'
- '\bbun\s+(install|i|add)\s+[^\n]*\bresend\b'
- '\byarn\s+add\s+[^\n]*\bresend\b'
- '\bnpm\s+(install|i|add)\s+[^\n]*@react-email/'
- '\bpnpm\s+(install|i|add)\s+[^\n]*@react-email/'
- '\bbun\s+(install|i|add)\s+[^\n]*@react-email/'
- '\byarn\s+add\s+[^\n]*@react-email/'
- '\bnpm\s+(install|i|add)\s+[^\n]*react-email\b'
- '\bpnpm\s+(install|i|add)\s+[^\n]*react-email\b'
- '\bbun\s+(install|i|add)\s+[^\n]*react-email\b'
- '\byarn\s+add\s+[^\n]*react-email\b'
retrieval:
aliases: # ← Alternate search terms
- email
- resend
- react email
- transactional email
intents: # ← What the user is trying to do
- send email from app
- set up resend integration
- create email template
- configure email domain verification
---
# Email Integration (Resend + React Email)
...guidance body follows...
What makes this skill a good minimal example:
- No
promptSignals— the skill relies entirely on file path and bash command matching. This is fine for highly specific tools where the file paths are unambiguous. - No
validaterules — email templates don't have common antipatterns that need automated checking. retrievalprovides discoverability without affecting runtime injection scoring.priority: 4is low, meaning other skills (nextjs at 5, storage at 7) will be injected first when budget is tight.
Pattern Matching Reference
pathPatterns (Globs)
File path globs are compiled to regex at build time via globToRegex(). Supported syntax:
| Pattern | Matches | Example |
|---|---|---|
* |
Any characters except / |
*.ts matches foo.ts but not dir/foo.ts |
** |
Any path depth (including zero) | app/**/*.tsx matches app/page.tsx and app/a/b/page.tsx |
? |
Single character | ?.ts matches a.ts but not ab.ts |
{a,b} |
Alternation | *.{ts,tsx} matches foo.ts and foo.tsx |
[abc] |
Character class | [._]env matches .env and _env |
Runtime matching strategy (PreToolUse tries in order):
- Full path: The compiled regex tests against the complete relative file path
- Basename: The regex tests against just the filename
- Suffix: The path ends with the glob pattern
bashPatterns (Regex)
Bash patterns are JavaScript regular expressions tested against the full bash command string.
bashPatterns:
- "\\bnext\\s+(dev|build|start)\\b" # next dev, next build, next start
- "npm run (dev|build)" # npm scripts
- "vercel\\s+deploy" # vercel deploy command
Tips:
- Use
\\bfor word boundaries (YAML requires escaping the backslash) - Use alternation
(a|b)for command variants - Patterns are case-sensitive by default
- Cover all package manager variants if matching install commands (npm/pnpm/bun/yarn)
importPatterns (Package Matchers)
Import patterns match against import/require statements in file content. They support wildcard scoping:
importPatterns:
- "ai" # Exact: import { x } from "ai"
- "@ai-sdk/*" # Scoped wildcard: @ai-sdk/openai, @ai-sdk/anthropic
- "@vercel/edge-config" # Exact scoped package
At build time, importPatternToRegex() compiles these into regex patterns with appropriate flags, stored in the manifest as { source, flags } pairs.
Prompt Signal Scoring
The UserPromptSubmit hook normalizes the user's prompt before scoring:
- Lowercased — all comparisons are case-insensitive
- Contraction expansion — "don't" → "do not", "isn't" → "is not", etc.
- Whitespace normalization — multiple spaces collapsed to a single space
Then each skill's promptSignals are evaluated:
flowchart TD
PROMPT["User prompt text"] --> NORM["Normalize:<br/>lowercase, expand contractions,<br/>collapse whitespace"]
NORM --> NONE{"noneOf<br/>matches?"}
NONE -->|"Yes"| SUPPRESS["-∞ → hard suppress"]
NONE -->|"No"| PHRASES["phrases: +6 each"]
PHRASES --> ALLOF["allOf: +4 per complete group"]
ALLOF --> ANYOF["anyOf: +1 each, max +2"]
ANYOF --> LEXICAL["Lexical fallback:<br/>+2 if skill terms overlap"]
LEXICAL --> TOTAL{"total ≥ minScore?"}
TOTAL -->|"Yes"| INJECT["Inject skill<br/>(up to 2 skills, 8KB budget)"]
TOTAL -->|"No"| SKIP["Skip"]
Scoring walkthrough
Given a prompt: "I want to add the ai sdk for streaming"
And a skill with:
promptSignals:
phrases: ["ai sdk"] # "ai sdk" is a substring → +6
allOf: [["streaming", "generation"]] # Only "streaming" matched, not "generation" → +0
anyOf: ["streaming"] # "streaming" present → +1
minScore: 6
| Component | Matches | Score |
|---|---|---|
| phrases: "ai sdk" | Yes (substring) | +6 |
| allOf: ["streaming", "generation"] | Partial (1 of 2) | +0 |
| anyOf: "streaming" | Yes | +1 |
| Total | 7 |
7 ≥ minScore 6 → skill injects.
Lexical fallback scoring
When phrase/allOf/anyOf scoring yields a low result, the system tokenizes both the prompt and skill metadata (description, phrases, aliases, entities) and checks for significant term overlap. This adds up to +2 and catches prompts that are topically relevant but don't match exact phrases.
Validation Rules
Validation rules run in the PostToolUse hook after Claude writes or edits a file. The hook:
- Matches the written file path against all skills'
pathPatterns - For each matched skill, runs its
validaterules against the file content - Returns fix instructions as
additionalContextfor any violations
flowchart TD
WRITE["Claude writes/edits file"] --> MATCH["Match file path → skills"]
MATCH --> LOOP["For each matched skill"]
LOOP --> RULE["For each validate rule"]
RULE --> SKIP{"skipIfFileContains<br/>matches?"}
SKIP -->|"Yes"| NEXT["Skip rule"]
SKIP -->|"No"| TEST{"pattern<br/>matches file?"}
TEST -->|"Yes"| REPORT["Report: severity + message"]
TEST -->|"No"| NEXT
NEXT --> RULE
Best practices for validation rules
- Use
severity: errorsparingly — only for patterns that will break functionality - Use
severity: warnfor style preferences or potential issues - Use
skipIfFileContainsto avoid false positives (e.g., skip "needs use client" if file already has it) - Keep
messageactionable — tell Claude what to do, not just what's wrong - Remember patterns are regex: escape special characters (
.→\\.,(→\\()
Real example: Next.js validation
validate:
# Error: will definitely break at runtime
- pattern: (?<!await )\bcookies\(\s*\)
message: 'cookies() is async in Next.js 16 — add await: const cookieStore = await cookies()'
severity: error
skipIfFileContains: "^['\"]use client['\"]"
# Warn: might be intentional, but usually a mistake
- pattern: (useState|useEffect)
message: 'React hooks require "use client" directive — add it at the top of client components'
severity: warn
skipIfFileContains: "^['\"]use client['\"]"
Manifest Build Pipeline
Skills are the source of truth, but hooks don't parse SKILL.md at runtime. Instead, a build step pre-compiles everything into a fast-lookup manifest.
flowchart LR
SKILLS["skills/*/SKILL.md<br/>(43 skills)"] -->|"build-manifest.ts"| MANIFEST["generated/skill-manifest.json"]
MANIFEST -->|"imported by"| HOOKS["Runtime hooks<br/>(pretooluse, user-prompt-submit)"]
Pipeline: SKILL.md → build-manifest.ts → skill-manifest.json
Input: All skills/*/SKILL.md files.
Processing (scripts/build-manifest.ts):
- Parse frontmatter — extracts YAML between
---delimiters using the custom parser - Compile pathPatterns — each glob is converted to a regex via
globToRegex(). Invalid globs are dropped (with warnings). - Compile bashPatterns — each string is validated as a
RegExp. Invalid patterns are dropped. - Compile importPatterns — each pattern is converted via
importPatternToRegex(), producing{ source, flags }pairs. - Paired arrays — the manifest stores patterns and their compiled regex sources in parallel arrays with matching indices. If a pattern fails to compile, both the pattern and its regex slot are dropped to keep indices aligned.
Output (generated/skill-manifest.json, version 2):
{
"generatedAt": "2026-03-10T...",
"version": 2,
"skills": {
"nextjs": {
"priority": 5,
"summary": null,
"pathPatterns": ["next.config.*", "app/**", ...],
"pathRegexSources": ["^next\\.config\\.[^/]*$", ...],
"bashPatterns": ["\\bnext\\s+(dev|build|start)\\b", ...],
"bashRegexSources": ["\\bnext\\s+(dev|build|start)\\b", ...],
"importPatterns": [],
"importRegexSources": [],
"bodyPath": "skills/nextjs/SKILL.md",
"validate": [...],
"promptSignals": { "phrases": [...], ... }
}
}
}
Build command
bun run build:manifest
This is also included in bun run build (which runs hooks + manifest + from-skills).
Keeping the manifest in sync
- Run
bun run validateto check manifest parity - Run
bun run doctorfor a comprehensive health check including manifest drift detection - The pre-commit hook does not auto-rebuild the manifest (only hooks are auto-compiled)
Template Include Engine
Skills are the single source of truth for domain knowledge. Agents and commands pull content from skills at build time via .md.tmpl templates, so they stay in sync without duplicating prose.
Section Includes
Extract a markdown section by heading:
{{include:skill:<skill-name>:<heading>}}
Behavior: Finds the heading (case-insensitive) in the skill's markdown body and extracts everything from that heading to the next heading of equal or higher level. Code blocks are skipped during heading detection.
Example: Given skills/nextjs/SKILL.md contains:
## App Router
Use the App Router for all new projects...
### File Conventions
- `page.tsx` — route entry point
- `layout.tsx` — shared layout
## Pages Router
Legacy approach...
Then {{include:skill:nextjs:App Router}} extracts:
## App Router
Use the App Router for all new projects...
### File Conventions
- `page.tsx` — route entry point
- `layout.tsx` — shared layout
It stops at ## Pages Router because that's an equal-level heading.
Nested headings are supported with > separator: {{include:skill:env-vars:vercel env CLI > List Environment Variables}} extracts a subsection under a parent heading.
Frontmatter Includes
Extract a frontmatter field value:
{{include:skill:<skill-name>:frontmatter:<field>}}
Supports dotted paths for nested fields:
{{include:skill:nextjs:frontmatter:metadata.priority}} → "5"
{{include:skill:email:frontmatter:description}} → "Email sending integration guidance..."
Build Workflow
# Compile all .md.tmpl templates → .md files
bun run build:from-skills
# Check if generated .md files are up-to-date (CI mode, exits non-zero on drift)
bun run build:from-skills:check
Current templates (8 files):
| Template | Output |
|---|---|
agents/ai-architect.md.tmpl |
agents/ai-architect.md |
agents/deployment-expert.md.tmpl |
agents/deployment-expert.md |
agents/performance-optimizer.md.tmpl |
agents/performance-optimizer.md |
commands/bootstrap.md.tmpl |
commands/bootstrap.md |
commands/deploy.md.tmpl |
commands/deploy.md |
commands/env.md.tmpl |
commands/env.md |
commands/marketplace.md.tmpl |
commands/marketplace.md |
commands/status.md.tmpl |
commands/status.md |
Diagnostic codes (reported when includes fail):
| Code | Meaning |
|---|---|
SKILL_NOT_FOUND |
No skills/<name>/SKILL.md exists |
HEADING_NOT_FOUND |
Heading doesn't exist in the skill body |
FRONTMATTER_NOT_FOUND |
Field path doesn't exist in YAML |
STALE_OUTPUT |
Generated .md is out of date |
Dependency tracking: generated/build-from-skills.manifest.json records which templates depend on which skills, enabling incremental builds and CI staleness checks.
Custom YAML Parser Gotchas
The plugin uses a custom inline YAML parser (parseSimpleYaml in skill-map-frontmatter.mjs), not the standard js-yaml library. This parser has intentional non-standard behavior:
| Input | Standard YAML | This Parser |
|---|---|---|
null (bare) |
JavaScript null |
String "null" |
true (bare) |
Boolean true |
String "true" |
false (bare) |
Boolean false |
String "false" |
[unclosed |
Parse error | Scalar string "[unclosed" |
| Tab indent | Usually accepted | Parse error |
Key implications
- No JavaScript nulls — you never get
nullfrom frontmatter; everything is a string or number. - No booleans — don't rely on boolean coercion; build scripts handle this explicitly.
- Always close brackets — a missing
]silently turns your array into a useless string. - Spaces only — the parser deliberately rejects tabs to avoid ambiguous indentation.
Build & Test Workflow
After creating or modifying a skill:
# 1. Build manifest (compiles frontmatter → JSON)
bun run build:manifest
# 2. Validate all skills
bun run validate
# 3. Test with explain CLI
bun run scripts/explain.ts --file <your-file-pattern>
# 4. Run full test suite
bun test
# 5. Build everything (hooks + manifest + templates)
bun run build
# 6. Run doctor to check for issues
bun run doctor
The pre-commit hook automatically runs build:hooks when .mts files are staged, but you should manually run build:manifest when changing frontmatter.