sipeed/picoclaw · error

skill frontmatter is required

Error message

skill frontmatter is required

What it means

validateAppliedSkillBody checks that an applied skill body (after TrimSpace) begins with the exact 4-byte prefix "---\n" - the opening YAML frontmatter delimiter. The evolution apply pipeline refuses to write a skill file that lacks frontmatter, since name and description metadata are mandatory downstream. A body starting with anything else - prose, a heading, or even '---' followed by CRLF - fails this check.

Source

Thrown at pkg/evolution/apply.go:149

	}
	skillDir := filepath.Dir(skillPath)
	if err := os.Remove(skillDir); err != nil && !os.IsNotExist(err) && !isDirNotEmptyError(err) {
		return err
	}
	return nil
}

func isDirNotEmptyError(err error) bool {
	if err == nil {
		return false
	}
	return strings.Contains(strings.ToLower(err.Error()), "directory not empty")
}

func validateAppliedSkillBody(body, targetSkillName string, allowExtraFrontmatterFields bool) error {
	body = strings.TrimSpace(body)
	if !strings.HasPrefix(body, "---\n") {
		return fmt.Errorf("skill frontmatter is required")
	}
	if !strings.Contains(body, "\n# ") {
		return fmt.Errorf("skill heading is required")
	}
	frontmatter, _ := splitSkillFrontmatter(body)
	fields, err := parseSkillFrontmatterFields(frontmatter, allowExtraFrontmatterFields)
	if err != nil {
		return err
	}
	name := strings.TrimSpace(fields["name"])
	if name == "" {
		return fmt.Errorf("skill frontmatter name is required")
	}
	if name != targetSkillName {
		return fmt.Errorf("skill frontmatter name %q does not match target skill %q", name, targetSkillName)
	}
	if strings.TrimSpace(fields["description"]) == "" {
		return fmt.Errorf("skill frontmatter description is required")

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Prepend a minimal frontmatter block: '---\nname: <skill>\ndescription: <what it does>\n---\n'
  2. Normalize line endings to LF before applying (dos2unix, or strings.ReplaceAll(body, "\\r\\n", "\\n"))
  3. Strip any BOM/leading spaces so the file's first bytes are exactly '---' followed by a newline
  4. Run the same validation locally before submitting the change: check strings.HasPrefix(strings.TrimSpace(body), "---\\n")

Example fix

# before: body starts with a heading
# My Skill
Does things.

# after: frontmatter block first
---
name: my-skill
description: Does things.
---
# My Skill
Does things.
Defensive patterns

Strategy: validation

Validate before calling

func hasFrontmatter(body string) bool {
    return strings.HasPrefix(strings.TrimSpace(
        strings.ReplaceAll(body, "\r\n", "\n")), "---\n")
}

Try / catch

if err := apply.Change(body); err != nil {
    if strings.Contains(err.Error(), "skill frontmatter is required") {
        return fmt.Errorf("skill file must start with a --- frontmatter block: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Applying a ChangeKind replace/create whose skill markdown has no '---' block at the top; a body authored on Windows whose first line ends with \r\n (so the prefix is "---\r\n" and does not match "---\n"); frontmatter separated from the body but with a leading BOM or spaces before the first ---.

Common situations: LLM-generated or hand-edited skill content that begins directly with '# Title'; files saved with CRLF line endings; content pasted from a renderer that strips the frontmatter block; BOM added by Windows editors.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/7d8d1976eadc279a. Report an issue: GitHub.