DietrichGebert/ponytail · error · Error
skills/${name}/SKILL.md has no frontmatter
Error message
skills/${name}/SKILL.md has no frontmatter What it means
Thrown by sourceBody() in scripts/build-openclaw-skills.js when reading skills/${name}/SKILL.md. The function normalizes CRLF to LF, then runs /^---\n[\s\S]*?\n---\n?/ to detect a YAML frontmatter block at the very start of the file. The build strips this frontmatter from each source skill and re-renders the file with canonical frontmatter (name/description/homepage/license), so every source skill MUST begin with a frontmatter block even though it is later discarded. If the regex finds no match the function throws, aborting the whole openclaw skill build.
Source
Thrown at scripts/build-openclaw-skills.js:33
const ROOT = path.join(__dirname, '..');
const HOMEPAGE = 'https://github.com/DietrichGebert/ponytail';
const DESCRIPTIONS = {
'ponytail': 'Lazy senior dev mode for any coding task (write, refactor, fix, review): YAGNI, stdlib first, no unrequested abstractions. Not for non-coding requests.',
'ponytail-review': 'Review a diff for over-engineering. Finds what to delete: reinvented stdlib, needless deps, speculative abstractions. One line per finding.',
'ponytail-audit': 'Audit the whole repo for over-engineering. A ranked list of what to delete, simplify, or replace with stdlib or native features.',
'ponytail-debt': 'Harvest every ponytail: shortcut comment into one debt ledger, so deferrals get tracked instead of forgotten. One-shot report.',
'ponytail-gain': 'Show ponytail measured impact as a scoreboard: less code, less cost, more speed, from the benchmark medians. One-shot display.',
'ponytail-help': "Quick reference for ponytail's modes, skills, and commands. One-shot display.",
};
const NAMES = Object.keys(DESCRIPTIONS);
function sourceBody(name) {
const src = fs.readFileSync(path.join(ROOT, 'skills', name, 'SKILL.md'), 'utf8').replace(/\r\n/g, '\n');
const fm = src.match(/^---\n[\s\S]*?\n---\n?/);
if (!fm) throw new Error(`skills/${name}/SKILL.md has no frontmatter`);
return src.slice(fm[0].length);
}
function render(name) {
const desc = DESCRIPTIONS[name];
if (desc.length > 160 || desc.includes('\n') || desc.includes('"')) {
throw new Error(`description for ${name} must be one line, no quotes, under 160 chars`);
}
const frontmatter =
`---\nname: ${name}\ndescription: "${desc}"\nhomepage: ${HOMEPAGE}\nlicense: MIT\n---\n`;
return frontmatter + sourceBody(name);
}
function outPath(name) {
return path.join(ROOT, '.openclaw', 'skills', name, 'SKILL.md');
}
module.exports = { DESCRIPTIONS, NAMES, render, outPath, sourceBody };View on GitHub (pinned to 2ed6c52c9d)
Solutions
- Read the error's ${name}, then open skills/${name}/SKILL.md and prepend a YAML frontmatter block that starts on the first byte: a line with exactly '---', one or more key lines, then a closing line with exactly '---'.
- Confirm the file begins with '---' immediately followed by a newline (no leading spaces, no BOM). If a BOM is present, re-save the file as UTF-8 without BOM.
- If the file is missing entirely, create skills/${name}/SKILL.md with a minimal frontmatter block ('---\nplaceholder: true\n---\n') plus the skill body; the build will overwrite the frontmatter.
- Re-run node scripts/build-openclaw-skills.js and confirm it progresses past this skill.
Example fix
// before — skills/ponytail-review/SKILL.md (no frontmatter, starts with body) # Ponytail Review Review a diff for over-engineering... // after — prepend any frontmatter block (it is stripped and replaced at build) --- placeholder: true --- # Ponytail Review Review a diff for over-engineering...
Defensive patterns
Strategy: validation
Validate before calling
// Run before invoking sourceBody(name)
const fs = require('fs'), path = require('path');
function hasFrontmatter(root, name) {
const src = fs.readFileSync(path.join(root, 'skills', name, 'SKILL.md'), 'utf8').replace(/\r\n/g, '\n');
return /^---\n[\s\S]*?\n---\n?/.test(src);
}
for (const name of NAMES) {
if (!hasFrontmatter(ROOT, name)) {
throw new Error(`refusing to build: skills/${name}/SKILL.md lacks frontmatter`);
}
} Type guard
// Node has no runtime type to guard, but a structural check works:
const FRONTMATTER_RE = /^---\n[\s\S]*?\n---\n?/;
function hasValidFrontmatter(src) {
return typeof src === 'string' && FRONTMATTER_RE.test(src.replace(/\r\n/g, '\n'));
} Try / catch
try {
sourceBody(name);
} catch (e) {
if (String(e.message).includes('no frontmatter')) {
console.error(`skills/${name}/SKILL.md: prepend a YAML frontmatter block ('---\\n...\\n---\\n') at the very first byte`);
}
throw e;
} Prevention
- Add a pre-commit/CI lint that asserts every skills/*/SKILL.md matches /^---\n[\s\S]*?\n---\n?/ after CRLF normalization.
- Also strip a leading \uFEFF before the regex, since the script normalizes \r\n but not BOM.
- Keep DESCRIPTIONS and the skills/*/ directories in sync — a rename in one must update the other in the same commit.
When it happens
Trigger: Running node scripts/build-openclaw-skills.js (or any npm script / CI step that imports it) when one of the skills listed in DESCRIPTIONS has a SKILL.md that: is empty, starts with body text or a markdown heading instead of '---', begins with leading whitespace/BOM before '---', or uses '--- ' (trailing space) / '---\r' so it is not immediately followed by a plain newline. Also fires if a skill directory in DESCRIPTIONS has no SKILL.md at all (readFileSync throws ENOENT, but the no-frontmatter branch fires for present-yet-malformed files).
Common situations: A contributor copies a README-style markdown file into skills/<name>/SKILL.md and forgets frontmatter. A new skill is added to the DESCRIPTIONS map but its SKILL.md was never created or was left as a stub heading. A Windows editor saves the file with a UTF-8 BOM or CRLF, and the CRLF is normalized but a leading BOM is not stripped (the script only normalizes \r\n, not \uFEFF). Someone renames a skill folder without updating the file path, leaving the old path's file without frontmatter.
Related errors
AI-assisted analysis of DietrichGebert/ponytail@2ed6c52c9d (2026-08-12).
Data as JSON: /api/errors/2876324fa83fcf71.
Report an issue: GitHub.