{"record":{"id":"46ae329770a80f9f","repo":"charmbracelet/crush","slug":"unclosed-frontmatter","errorCode":null,"errorMessage":"unclosed frontmatter","messagePattern":"unclosed frontmatter","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/skills/skills.go","lineNumber":203,"sourceCode":"\t// Strip UTF-8 BOM for compatibility with editors that include it.\n\tcontent = strings.TrimPrefix(content, \"\\uFEFF\")\n\t// Normalize line endings to \\n for consistent parsing.\n\tcontent = strings.ReplaceAll(content, \"\\r\\n\", \"\\n\")\n\tcontent = strings.ReplaceAll(content, \"\\r\", \"\\n\")\n\n\tlines := strings.Split(content, \"\\n\")\n\tstart := slices.IndexFunc(lines, func(line string) bool {\n\t\treturn strings.TrimSpace(line) != \"\"\n\t})\n\tif start == -1 || strings.TrimSpace(lines[start]) != \"---\" {\n\t\treturn \"\", \"\", errors.New(\"no YAML frontmatter found\")\n\t}\n\n\tendOffset := slices.IndexFunc(lines[start+1:], func(line string) bool {\n\t\treturn strings.TrimSpace(line) == \"---\"\n\t})\n\tif endOffset == -1 {\n\t\treturn \"\", \"\", errors.New(\"unclosed frontmatter\")\n\t}\n\tend := start + 1 + endOffset\n\n\tfrontmatter = strings.Join(lines[start+1:end], \"\\n\")\n\tbody = strings.Join(lines[end+1:], \"\\n\")\n\treturn frontmatter, body, nil\n}\n\n// Discover finds all valid skills in the given paths.\nfunc Discover(paths []string) []*Skill {\n\tskills, _ := DiscoverWithStates(paths)\n\treturn skills\n}\n\n// DiscoverWithStates finds all valid skills in the given paths and also\n// returns a per-file state slice describing parse/validation outcomes. Useful\n// for diagnostics and UI reporting.\nfunc DiscoverWithStates(paths []string) ([]*Skill, []*SkillState) {","sourceCodeStart":185,"sourceCodeEnd":221,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/skills/skills.go#L185-L221","documentation":"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.","triggerScenarios":"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 `---`.","commonSituations":"Manual edits that removed the closing fence; heredoc/script generation that wrote only one delimiter; copy-paste truncating the file's end.","solutions":["Add the closing `---` line after the YAML keys","Ensure the closing delimiter is on its own line with no indentation or extra characters","Validate the file renders as frontmatter + body by re-running the parser after editing"],"exampleFix":"// before\n---\nname: my-skill\ndescription: Does things\n// after\n---\nname: my-skill\ndescription: Does things\n---","handlingStrategy":"validation","validationCode":"func frontmatterClosed(content string) bool {\n\tlines := strings.Split(content, \"\\n\")\n\tcount := 0\n\tfor _, l := range lines {\n\t\tif strings.TrimSpace(l) == \"---\" { count++ }\n\t}\n\treturn count >= 2\n}","typeGuard":null,"tryCatchPattern":"fm, body, err := skills.ParseContent(content)\nif err != nil {\n\treturn fmt.Errorf(\"unterminated frontmatter: %w\", err)\n}","preventionTips":["Always close the frontmatter block with a second `---`","Check file endings after scripted generation","Preview parsed skill files before committing"],"tags":["skills","frontmatter","parsing"],"backgroundTag":"missing-frontmatter","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}