Files
vercel__vercel-plugin/docs/skill-authoring.md
John Lindquist d5b5ef47f7 Replace skill body injection with Skill tool invocation instructions
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.
2026-03-10 12:46:11 -06:00

34 KiB
Raw Permalink Blame History

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

  1. User Story: Adding a New Skill End-to-End
  2. SKILL.md Frontmatter Schema
  3. Annotated Real Skill Examples
  4. Pattern Matching Reference
  5. Prompt Signal Scoring
  6. Validation Rules
  7. Manifest Build Pipeline
  8. Template Include Engine
  9. Custom YAML Parser Gotchas
  10. 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

  1. SessionStart — The profiler scans package.json. If @vercel/edge-config is a dependency, your skill gets a +5 priority boost via VERCEL_PLUGIN_LIKELY_SKILLS.
  2. PreToolUse — When Claude reads edge-config.json or runs vercel env pull, your pathPatterns and bashPatterns match → the skill is ranked, deduped, and injected within the 18KB budget (max 3 skills).
  3. UserPromptSubmit — When the developer types "how do I set up edge config?", your promptSignals.phrases score +6 → the skill injects within the 8KB budget (max 2 skills).
  4. PostToolUse — When Claude writes to a matched file, your validate rules 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 .tsx components for a while — adding hooks, state, and effects. After your 3rd .tsx edit, 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=1 if the CLI is found on PATH
  • A loop guard caps injection at 2 per session (VERCEL_PLUGIN_DEV_VERIFY_COUNT) — repeated npm run dev restarts won't flood context
  • The verification skill 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 workflow skill 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

  1. Start with phrases — these are the strongest signals (+6 each). Use the exact phrases your users type: "edge config", "ai sdk", "deploy to vercel".

  2. Add allOf groups for concepts — combinations of terms that together indicate intent: [["cron", "schedule"]], [["streaming", "response"]]. Each fully-matched group scores +4.

  3. Sprinkle anyOf for weak signals — individual terms that slightly boost relevance: "timeout", "cache", "optimize". Capped at +2 total to prevent noise.

  4. 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.

  5. Set minScore appropriately — default 6 (one phrase match). Lower to 4 for broad skills like investigation-mode that 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 48). 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/, or apps/*/app/.
  • promptSignals use all four scoring mechanisms: phrases for strong direct matches, allOf for multi-term concepts, anyOf for weaker signals, and minScore: 6 so a single phrase is sufficient.
  • validate rules target real migration pitfalls (Pages Router → App Router, React 18 → 19, Next.js 15 → 16) and use skipIfFileContains to 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 validate rules — email templates don't have common antipatterns that need automated checking.
  • retrieval provides discoverability without affecting runtime injection scoring.
  • priority: 4 is 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):

  1. Full path: The compiled regex tests against the complete relative file path
  2. Basename: The regex tests against just the filename
  3. 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 \\b for 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:

  1. Lowercased — all comparisons are case-insensitive
  2. Contraction expansion — "don't" → "do not", "isn't" → "is not", etc.
  3. 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:

  1. Matches the written file path against all skills' pathPatterns
  2. For each matched skill, runs its validate rules against the file content
  3. Returns fix instructions as additionalContext for 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: error sparingly — only for patterns that will break functionality
  • Use severity: warn for style preferences or potential issues
  • Use skipIfFileContains to avoid false positives (e.g., skip "needs use client" if file already has it)
  • Keep message actionable — 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.mdbuild-manifest.tsskill-manifest.json

Input: All skills/*/SKILL.md files.

Processing (scripts/build-manifest.ts):

  1. Parse frontmatter — extracts YAML between --- delimiters using the custom parser
  2. Compile pathPatterns — each glob is converted to a regex via globToRegex(). Invalid globs are dropped (with warnings).
  3. Compile bashPatterns — each string is validated as a RegExp. Invalid patterns are dropped.
  4. Compile importPatterns — each pattern is converted via importPatternToRegex(), producing { source, flags } pairs.
  5. 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 validate to check manifest parity
  • Run bun run doctor for 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

  1. No JavaScript nulls — you never get null from frontmatter; everything is a string or number.
  2. No booleans — don't rely on boolean coercion; build scripts handle this explicitly.
  3. Always close brackets — a missing ] silently turns your array into a useless string.
  4. 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.