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

  1. 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 '---'.
  2. 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.
  3. 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.
  4. 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

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.