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

847 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](#user-story-adding-a-new-skill-end-to-end)
2. [SKILL.md Frontmatter Schema](#skillmd-frontmatter-schema)
- [Top-Level Fields](#top-level-fields)
- [metadata Object](#metadata-object)
- [promptSignals Object](#promptsignals-object)
- [validate Array](#validate-array)
- [retrieval Object](#retrieval-object)
3. [Annotated Real Skill Examples](#annotated-real-skill-examples)
- [Example 1: nextjs (Complex — Signals + Validation)](#example-1-nextjs)
- [Example 2: email (Minimal — Patterns Only)](#example-2-email)
4. [Pattern Matching Reference](#pattern-matching-reference)
- [pathPatterns (Globs)](#pathpatterns-globs)
- [bashPatterns (Regex)](#bashpatterns-regex)
- [importPatterns (Package Matchers)](#importpatterns-package-matchers)
5. [Prompt Signal Scoring](#prompt-signal-scoring)
6. [Validation Rules](#validation-rules)
7. [Manifest Build Pipeline](#manifest-build-pipeline)
8. [Template Include Engine](#template-include-engine)
- [Section Includes](#section-includes)
- [Frontmatter Includes](#frontmatter-includes)
- [Build Workflow](#build-workflow)
9. [Custom YAML Parser Gotchas](#custom-yaml-parser-gotchas)
10. [Build & Test Workflow](#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
```mermaid
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
```bash
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](#skillmd-frontmatter-schema) section for every available field.
Minimal skeleton:
```markdown
---
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
```bash
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
```bash
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
```bash
# 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
```bash
bun test
```
### Step 7 — Build everything and commit
```bash
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**.
```mermaid
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.
```mermaid
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**:
```yaml
# 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 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.
```yaml
---
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.
```yaml
---
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.
```yaml
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:
```yaml
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:
```mermaid
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:
```yaml
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
```mermaid
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
```yaml
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.
```mermaid
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`):
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):
```json
{
"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
```bash
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:
```markdown
## 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:
```markdown
## 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
```bash
# 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:
```bash
# 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.