Parse and validate YAML frontmatter in markdown files for skills and agents
This skill provides expertise in parsing and validating YAML frontmatter from markdown files, specifically for Claude Code configuration files like skills and sub-agents.
YAML frontmatter is metadata at the beginning of markdown files, enclosed in triple dashes:
---
name: my-skill
description: A useful skill
version: 1.0.0
author: Developer Name
---
# Skill Content
The rest of the markdown content...
interface YAMLFrontmatter {
[key: string]: string | number | boolean | string[];
}
/**
* Extract YAML frontmatter from markdown content
*/
function extractFrontmatter(content: string): YAMLFrontmatter | null {
// Match YAML frontmatter pattern
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
const match = content.match(frontmatterRegex);
if (!match || !match[1]) {
return null;
}
const yamlContent = match[1];
return parseYAML(yamlContent);
}
/**
* Simple YAML parser for key-value pairs
* Note: This handles simple cases. For complex YAML, use a library like js-yaml
*/
function parseYAML(yaml: string): YAMLFrontmatter {
const result: YAMLFrontmatter = {};
const lines = yaml.split('\n');
for (const line of lines) {
// Skip empty lines and comments
if (!line.trim() || line.trim().startsWith('#')) {
continue;
}
// Parse key-value pair
const colonIndex = line.indexOf(':');
if (colonIndex === -1) {
continue;
}
const key = line.substring(0, colonIndex).trim();
let value: string | number | boolean = line.substring(colonIndex + 1).trim();
// Remove quotes
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
// Parse booleans
if (value === 'true') value = true;
if (value === 'false') value = false;
// Parse numbers
if (!isNaN(Number(value)) && value !== '') {
value = Number(value);
}
result[key] = value;
}
return result;
}
import * as fs from 'fs/promises';
// Read markdown file
const content = await fs.readFile('skill.md', 'utf-8');
// Extract frontmatter
const frontmatter = extractFrontmatter(content);
if (frontmatter) {
console.log('Name:', frontmatter.name);
console.log('Description:', frontmatter.description);
console.log('Version:', frontmatter.version);
}
// Remove frontmatter to get body content
const body = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, '');
---
name: skill-name # Required: Skill identifier
description: Brief description # Required: One-line description
version: 1.0.0 # Optional: Semantic version
author: Author Name # Optional: Author information
tags: [tag1, tag2] # Optional: Categories/tags
dependencies: [other-skill] # Optional: Required skills
---
Validation:
interface SkillFrontmatter {
name: string;
description: string;
version?: string;
author?: string;
tags?: string[];
dependencies?: string[];
}
function validateSkillFrontmatter(fm: YAMLFrontmatter): SkillFrontmatter {
if (!fm.name || typeof fm.name !== 'string') {
throw new Error('Skill frontmatter must include "name" field');
}
if (!fm.description || typeof fm.description !== 'string') {
throw new Error('Skill frontmatter must include "description" field');
}
return {
name: fm.name,
description: fm.description,
version: fm.version as string | undefined,
author: fm.author as string | undefined,
tags: Array.isArray(fm.tags) ? fm.tags : undefined,
dependencies: Array.isArray(fm.dependencies) ? fm.dependencies : undefined,
};
}
---
name: agent-name # Required: Agent identifier
color: blue # Optional: Icon color (red, blue, green, purple, yellow, orange)
description: Brief description # Optional: Agent purpose
model: sonnet # Optional: Preferred model
tools: [Read, Write, Bash] # Optional: Tool restrictions
---
Validation:
interface AgentFrontmatter {
name: string;
color?: 'red' | 'blue' | 'green' | 'purple' | 'yellow' | 'orange' | 'default';
description?: string;
model?: 'sonnet' | 'opus' | 'haiku';
tools?: string[];
}
function validateAgentFrontmatter(fm: YAMLFrontmatter): AgentFrontmatter {
if (!fm.name || typeof fm.name !== 'string') {
throw new Error('Agent frontmatter must include "name" field');
}
// Validate color
const validColors = ['red', 'blue', 'green', 'purple', 'yellow', 'orange', 'default'];
if (fm.color && !validColors.includes(fm.color as string)) {
throw new Error(`Invalid color: ${fm.color}. Must be one of: ${validColors.join(', ')}`);
}
return {
name: fm.name,
color: (fm.color as AgentFrontmatter['color']) || 'default',
description: fm.description as string | undefined,
model: fm.model as AgentFrontmatter['model'] | undefined,
tools: Array.isArray(fm.tools) ? fm.tools : undefined,
};
}
interface MarkdownFile {
frontmatter: YAMLFrontmatter | null;
body: string;
}
async function readMarkdownWithFrontmatter(filePath: string): Promise<MarkdownFile> {
const content = await fs.readFile(filePath, 'utf-8');
const frontmatter = extractFrontmatter(content);
const body = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, '');
return { frontmatter, body };
}
function serializeYAML(data: YAMLFrontmatter): string {
const lines: string[] = [];
for (const [key, value] of Object.entries(data)) {
if (Array.isArray(value)) {
lines.push(`${key}: [${value.join(', ')}]`);
} else if (typeof value === 'string') {
// Quote strings with special characters
const needsQuotes = value.includes(':') || value.includes('#') || value.includes('\n');
lines.push(`${key}: ${needsQuotes ? `"${value}"` : value}`);
} else {
lines.push(`${key}: ${value}`);
}
}
return lines.join('\n');
}
async function writeMarkdownWithFrontmatter(
filePath: string,
frontmatter: YAMLFrontmatter,
body: string
): Promise<void> {
const yaml = serializeYAML(frontmatter);
const content = `---\n${yaml}\n---\n\n${body}`;
await fs.writeFile(filePath, content, 'utf-8');
}
async function updateFrontmatter(
filePath: string,
updates: Partial<YAMLFrontmatter>
): Promise<void> {
const { frontmatter, body } = await readMarkdownWithFrontmatter(filePath);
const updatedFrontmatter = {
...(frontmatter || {}),
...updates,
};
await writeMarkdownWithFrontmatter(filePath, updatedFrontmatter, body);
}
// Usage
await updateFrontmatter('agent.md', { color: 'blue', version: '1.1.0' });
function safeExtractFrontmatter(content: string): YAMLFrontmatter | null {
try {
const fm = extractFrontmatter(content);
if (!fm) {
console.warn('No frontmatter found in content');
return null;
}
return fm;
} catch (error) {
console.error('Failed to parse frontmatter:', error);
return null;
}
}
async function safeReadSkill(filePath: string): Promise<SkillFrontmatter | null> {
try {
const { frontmatter } = await readMarkdownWithFrontmatter(filePath);
if (!frontmatter) {
return null;
}
return validateSkillFrontmatter(frontmatter);
} catch (error) {
console.error(`Failed to read skill at ${filePath}:`, error);
return null;
}
}
// In TreeDataProvider
async getChildren(): Promise<SkillTreeItem[]> {
const skillFiles = await this.discoverSkillFiles();
const items: SkillTreeItem[] = [];
for (const file of skillFiles) {
try {
const { frontmatter } = await readMarkdownWithFrontmatter(file.path);
items.push({
label: frontmatter?.name || file.name,
description: frontmatter?.description,
tooltip: this.createTooltip(frontmatter),
path: file.path,
});
} catch (error) {
// Handle parsing error gracefully
items.push({
label: file.name,
description: 'Error parsing frontmatter',
path: file.path,
});
}
}
return items;
}
private createTooltip(fm: YAMLFrontmatter | null): string {
if (!fm) return 'No metadata';
const parts: string[] = [];
if (fm.name) parts.push(`**${fm.name}**`);
if (fm.description) parts.push(fm.description);
if (fm.version) parts.push(`Version: ${fm.version}`);
if (fm.author) parts.push(`Author: ${fm.author}`);
return parts.join('\n\n');
}
function getAgentIconColor(color: string): vscode.ThemeColor {
const colorMap: Record<string, string> = {
red: 'charts.red',
blue: 'charts.blue',
green: 'charts.green',
purple: 'charts.purple',
yellow: 'charts.yellow',
orange: 'charts.orange',
default: 'foreground',
};
return new vscode.ThemeColor(colorMap[color] || colorMap.default);
}
// In getTreeItem
const { frontmatter } = await readMarkdownWithFrontmatter(element.path);
const color = frontmatter?.color || 'default';
treeItem.iconPath = new vscode.ThemeIcon('account', getAgentIconColor(color));
For complex YAML (nested objects, multi-line strings), use a proper YAML library:
import * as yaml from 'js-yaml';
function extractFrontmatterAdvanced(content: string): YAMLFrontmatter | null {
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
const match = content.match(frontmatterRegex);
if (!match || !match[1]) {
return null;
}
try {
return yaml.load(match[1]) as YAMLFrontmatter;
} catch (error) {
console.error('YAML parsing error:', error);
return null;
}
}
This skill provides:
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer