charmbracelet/crush · error

unclosed frontmatter

Error message

unclosed frontmatter

What it means

splitFrontmatter throws this error when it finds an opening `---` line but no closing `---` line anywhere after it, so the frontmatter block is unterminated and the YAML cannot be extracted.

Source

Thrown at internal/skills/skills.go:203

	// Strip UTF-8 BOM for compatibility with editors that include it.
	content = strings.TrimPrefix(content, "\uFEFF")
	// Normalize line endings to \n for consistent parsing.
	content = strings.ReplaceAll(content, "\r\n", "\n")
	content = strings.ReplaceAll(content, "\r", "\n")

	lines := strings.Split(content, "\n")
	start := slices.IndexFunc(lines, func(line string) bool {
		return strings.TrimSpace(line) != ""
	})
	if start == -1 || strings.TrimSpace(lines[start]) != "---" {
		return "", "", errors.New("no YAML frontmatter found")
	}

	endOffset := slices.IndexFunc(lines[start+1:], func(line string) bool {
		return strings.TrimSpace(line) == "---"
	})
	if endOffset == -1 {
		return "", "", errors.New("unclosed frontmatter")
	}
	end := start + 1 + endOffset

	frontmatter = strings.Join(lines[start+1:end], "\n")
	body = strings.Join(lines[end+1:], "\n")
	return frontmatter, body, nil
}

// Discover finds all valid skills in the given paths.
func Discover(paths []string) []*Skill {
	skills, _ := DiscoverWithStates(paths)
	return skills
}

// DiscoverWithStates finds all valid skills in the given paths and also
// returns a per-file state slice describing parse/validation outcomes. Useful
// for diagnostics and UI reporting.
func DiscoverWithStates(paths []string) ([]*Skill, []*SkillState) {

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Add the closing `---` line after the YAML keys
  2. Ensure the closing delimiter is on its own line with no indentation or extra characters
  3. Validate the file renders as frontmatter + body by re-running the parser after editing

Example fix

// before
---
name: my-skill
description: Does things
// after
---
name: my-skill
description: Does things
---
Defensive patterns

Strategy: validation

Validate before calling

func frontmatterClosed(content string) bool {
	lines := strings.Split(content, "\n")
	count := 0
	for _, l := range lines {
		if strings.TrimSpace(l) == "---" { count++ }
	}
	return count >= 2
}

Try / catch

fm, body, err := skills.ParseContent(content)
if err != nil {
	return fmt.Errorf("unterminated frontmatter: %w", err)
}

Prevention

When it happens

Trigger: A SKILL.md with `---` on the first non-blank line but no second `---` delimiter; the closing delimiter accidentally deleted during editing; closing delimiter indented or written with extra characters so it no longer trims to `---`.

Common situations: Manual edits that removed the closing fence; heredoc/script generation that wrote only one delimiter; copy-paste truncating the file's end.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/46ae329770a80f9f. Report an issue: GitHub.