Repository navigation
Description
Summary
Project skills (and potentially agents/commands parsed via ConfigMarkdown.parse) silently vanish whenever the same markdown content is parsed more than once in a single server process and its frontmatter is not strict-YAML-valid (e.g. an unquoted description: containing : ).
Root cause: gray-matter 4.0.3 registers its module-level cache entry before parsing. When strict YAML parsing throws, the never-parsed entry (data: {}) stays in matter.cache keyed by the file content. opencode's sanitize() fallback rescues the first parse per unique content per process — but every later matter(content) call hits the poisoned cache entry, returns data: {} without throwing (so the fallback never runs), and Skill.add() drops the file silently at the isSkillFrontmatter guard.
This looks like the actual root cause behind #41751 ("exactly N skills silently dropped in server mode") and plausibly #41213.
Minimal repro (gray-matter level)
import matter from "gray-matter"
const s = `---\nname: x\ndescription: Use this. Trigger keywords: "e2e", "test"\n---\nbody`
try { matter(s) } catch (e) { /* YAMLException — opencode falls back to sanitize() here */ }
console.log(matter(s).data) // => {} <- poisoned cache: no throw, no data, skill silently droppedpackages/core/src/config/markdown.ts:
export function parse(content: string) {
try {
return matter(content) // 1st call: throws AND poisons matter.cache[content]
} catch {
return matter(sanitize(content)) // rescues the 1st call only
}
}gray-matter 4.0.3, index.js:
let file = toFile(input);
const cached = matter.cache[file.content];
if (!options) {
if (cached) { ...return shallow copy... }
matter.cache[file.content] = file; // <- cached BEFORE parseMatter() runs
}
return parseMatter(file, options); // <- throws on strict-YAML errors, cache entry stays emptyServer-level repro (v1.18.16, also reproduced on 1.18.18 via bunx [email protected])
- A project with skills whose
description:contains an unquoted:(very common — e.g.... Trigger keywords: "foo", "bar"). opencode serve --port 4097(with or withoutOPENCODE_EXPERIMENTAL_WORKSPACES).curl -H "x-opencode-directory: <project>" :4097/skill→ all skills present (first discovery: throw → sanitize fallback works).curl -X POST -H "x-opencode-directory: <project>" :4097/instance/dispose, thenGET /skillagain → all affected project skills gone (count drops from e.g. 66 to 42), reproducible 15/15 times. No error or warning in logs —init count=…simply shrinks.
Because the cache is keyed by content, the poisoning also crosses instances and directories:
- Mirror layouts (
.claude/skills/.agents/skillssymlinked to.opencode/skills): the 3 scans see identical content; only the first-parsed path survives — this is also why noduplicate skill namewarnings appear for project mirrors. - Sibling forks: once repo A's
add-e2e-test/SKILL.mdwas parsed, repo B's byte-identical copy loads asdata:{}— opening a second project in the same server silently loses its skills. We measured fresh instances loading 0/12, 3/15, 6/20, 1/13 of their skills depending on how much identical content earlier instances had already parsed (the survivors were exactly the files with unique content). - CLI/TUI single-shot runs are mostly unaffected (one discovery per process), matching the "works in CLI, breaks in server mode" observation in v1.18.16 server/web mode: exactly 2 project skills silently dropped when project is a git repo (works in CLI, works without .git) #41751.
Suggested fix
Bypass gray-matter's cache in ConfigMarkdown.parse — any truthy options object disables it:
export function parse(content: string) {
try {
return matter(content, {}) // no cache read/write -> no poisoning
} catch {
return matter(sanitize(content), {})
}
}(Or delete matter.cache[content] before the fallback.) Skill discovery re-parses at most a few hundred small files per instance creation, so losing the cache is negligible. Additionally, Skill.add()'s isSkillFrontmatter failure path could log a warning instead of silently returning — that would have made this diagnosable from logs.
Workaround for affected users
Make skill frontmatter strict-YAML valid, e.g. convert unquoted colon-containing descriptions to block scalars (description: |-). After that, the first parse never throws, nothing is poisoned, and server-mode discovery is stable across /instance/dispose (verified: 8/8 full loads on the same previously-affected server).
Environment
- opencode v1.18.16 (homebrew) and v1.18.18 (
bunx opencode-ai), macOS arm64, Bun 1.3.14 - gray-matter 4.0.3 (as pinned by
packages/core)
Activity
enterdot commentedon Sep 19, 2026
I can confirm this, also hit it on V2 (2.0.8). Also, it's not only skills that vanish, subagents silently fall back to
mode: primaryandhidden: falseafter the first reload, because every field in the agent schema is optional and the empty frontmatter just decodes to defaults. Quoting the affected frontmatter values works around it.thdxr commentedon Oct 11, 2026
Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (
opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊