anomalyco / anomalyco/opencode
Skills silently vanish on re-parse: gray-matter cache is poisoned when strict-YAML frontmatter parse throws
@nexxeln is already working on this.
Since Aug 13, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
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 dropped
packages/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 empty
Server-level repro (v1.18.16, also reproduced on 1.18.18 via bunx opencode-ai@1.18.18)
- 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 #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)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.