Repository navigation
Description
Description
In a long-running opencode serve process, SKILL.md files whose frontmatter is invalid YAML are dropped silently — no error, no warning, no log line. The failure is also inconsistent per instance: the first Skill instance init loads them (via the sanitize() fallback in packages/core/src/config/markdown.ts), but every subsequent init of the same files in the same process drops them.
Observed in 1.18.32: 3 skills whose description: plain scalar contained an unquoted : (e.g. description: Use when the user needs to ... Keywords: crawl, scrape) were on disk. First instance init logged init { count: 22 }, second logged init { count: 19 }, and nothing else appeared in the log. The skills disappeared from available_skills with no diagnostic.
Root cause (all verified against v1.18.32 sources):
- gray-matter writes to its module-global content cache before parsing (
gray-matter/index.js):matter.cache[file.content] = fileruns beforeparseMatter(), so a failed parse leaves a cache entry with no usabledata. Upstream: gray-matter#166 (open since 2023). packages/core/src/config/markdown.tscallsmatter(content)without options → the 2nd+ load returns the poisoned entry without throwing → thesanitize()fallback never runs. (Note:sanitize()already rescues unquoted-colon frontmatter on the first load — the poisoning is what breaks subsequent loads.)packages/opencode/src/skill/index.ts(add):if (!isSkillFrontmatter(md.data)) return— silent, no log.
Suggested fix (not yet tested — happy to PR once verified):
markdown.ts: callmatter(content, {})— passing any options makes gray-matter skip the cache (read and write), so a failed parse can no longer poison later loads.skill/index.ts: log a warning whenisSkillFrontmatter(md.data)fails, so drops are visible.- Docs gap: the built-in
customize-opencodeskill (packages/core/src/plugin/skill/customize-opencode.md,## Skills) recommends long plain-scalar descriptions without stating that frontmatter must be valid YAML — a:inside a plain scalar is invalid and kills the whole file. Suggest adding: quote the value or use a>-block scalar for multi-clause descriptions.
Related: #24181 (same symptom family via a different mechanism, agent frontmatter; closed as not_planned).
Steps to reproduce
-
Minimal gray-matter repro (any Node with gray-matter):
const matter = require("gray-matter") const c = "---\nname: x\ndescription: Use when: foo\n---\nbody" matter(c) // throws YAMLException matter(c) // returns { data: {}, ... } — no throw
-
opencode-level:
- Create 3
SKILL.mdfiles whosedescription:plain scalar contains:. - Run
opencode servesuch that twoSkillinstances initialize in one process (e.g. two different working directories — each gets its ownInstanceState). - First init:
INFO init { count: N+3 }— broken skills load via thesanitize()fallback. - Second init:
INFO init { count: N }— same files, no error/warning anywhere in the log. - New sessions in the second instance are missing those skills.
- Create 3
Plugins
none
OpenCode version
1.18.32
Screenshot and/or share link
n/a (server logs: init { count: 22 } followed by init { count: 19 }, no other skill-related lines)
Operating System
Linux (headless opencode serve in a node:24-alpine container)
Terminal
headless (server + web UI)
Activity
holny commentedon Sep 25, 2026
I'll take this. Confirmed on current v2 — same mechanism, same silent drop:
matter(content)inpackages/core/src/config/markdown.ts:6is called without options, so gray-matter uses its module-global content cache, which it writes before parsing (gray-matter/index.js:47). A failed YAML parse leaves a poisoned entry, and every laterparse()with the same content returns it without throwing — so thesanitize()fallback at markdown.ts:8 never runs and the skill is dropped by the empty-data check upstream.Fix is one line: call
matter(content, {}). Passing any options object bypasses the cache read and write (gray-matter only caches when called with no options), so every parse is deterministic and the fallback works on each load. Add a regression test that parses invalid frontmatter twice and asserts the second parse still throws (and the sanitized retry still recovers).Fixes #51218