Hmbown/CodeWhale · error · anyhow::Error

SKILL.md is missing the closing '---' frontmatter fence

Error message

SKILL.md is missing the closing '---' frontmatter fence

What it means

parse_frontmatter_name validates a SKILL.md before install: the file must be valid UTF-8, start with a '---' fence, and contain a second '---' that closes the frontmatter block. This error fires when the opening fence exists but no closing '---' is found anywhere after it, so the frontmatter (name/description) cannot be delimited. The search is a plain find of '---' in the remainder, so the closing fence must appear on its own before the body.

Source

Thrown at crates/tui/src/skills/install.rs:1598

        std::borrow::Cow::Owned(rest.to_string())
    } else if path == prefix {
        std::borrow::Cow::Borrowed("")
    } else {
        std::borrow::Cow::Borrowed(path)
    }
}

/// Extract `name:` and ensure `description:` exist in the SKILL.md frontmatter.
/// Also verifies the leading `---` fence so we reject malformed files early.
fn parse_frontmatter_name(bytes: &[u8]) -> Result<String> {
    let content = std::str::from_utf8(bytes).context("SKILL.md is not valid UTF-8")?;
    let trimmed = content.trim_start();
    if !trimmed.starts_with("---") {
        bail!("SKILL.md is missing the leading '---' frontmatter fence");
    }
    let after_open = &trimmed[3..];
    let close = after_open.find("---").ok_or_else(|| {
        anyhow::anyhow!("SKILL.md is missing the closing '---' frontmatter fence")
    })?;
    let frontmatter = &after_open[..close];

    let mut name: Option<String> = None;
    let mut has_description = false;
    for raw in frontmatter.lines() {
        let line = raw.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        if let Some((key, value)) = line.split_once(':') {
            let key = key.trim().to_ascii_lowercase();
            let value = value.trim().to_string();
            match key.as_str() {
                "name" if !value.is_empty() => name = Some(value),
                "description" if !value.is_empty() => has_description = true,
                _ => {}
            }

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Open SKILL.md and add a line containing only '---' after the YAML frontmatter (after name: and description: lines).
  2. Verify structure: line 1 '---', frontmatter keys, then a closing '---' line, then the body.
  3. If the file intentionally has no frontmatter, add a minimal block: '---\nname: my-skill\ndescription: ...\n---'.
  4. Re-run the install; the fence check happens before any files are copied.

Example fix

# before (SKILL.md)
---
name: my-skill
description: Does a thing.

# My Skill
...

# after
---
name: my-skill
description: Does a thing.
---

# My Skill
...
Defensive patterns

Strategy: validation

Validate before calling

fn frontmatter_fences_ok(md: &str) -> bool {
    let t = md.trim_start();
    t.starts_with("---") && t[3..].find("---").is_some()
}

let md = std::fs::read_to_string(skill_md_path)?;
anyhow::ensure!(frontmatter_fences_ok(&md), "SKILL.md frontmatter fences malformed");

Type guard

function hasClosedFrontmatter(md: string): boolean {
  const t = md.replace(/^\s+/, '');
  if (!t.startsWith('---')) return false;
  return t.slice(3).includes('---');
}

Prevention

When it happens

Trigger: Installing a skill whose SKILL.md begins with '---' but never closes the frontmatter block — e.g. only one fence at the top, or frontmatter that runs to EOF. The opener check passes (error would otherwise be the 'leading fence' variant), then after_open.find("---") returns None.

Common situations: Hand-authored SKILL.md missing the second fence; a markdown file converted from a template that uses '---' as a horizontal rule; truncated file where the closing fence was cut off.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/a251bb525d6b08df. Report an issue: GitHub.