Skip to content

Bug: frontmatter sanitizer skips hyphenated keys, so allowed-tools breaks command/agent files #39603

Description

@LHMQ878

Description

sanitize() in packages/core/src/config/markdown.ts exists so that "other coding agents accept unquoted colons in frontmatter values" keep working (comment on line 20). But its key pattern is:

const entry = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/)

The character class has no -, so any hyphenated key fails to match, hits if (!entry) return [line], and is returned unsanitized. The retry matter(sanitize(content)) then throws exactly as the first attempt did.

This is unfortunate because allowed-tools is the single most common such key in other agents' command files - precisely the compatibility case the function was written for (see #6985 for .claude/commands/ compatibility).

Steps to reproduce

.opencode/command/review.md:

---
allowed-tools: Read: every file
description: Review helper
---
Review my code.

Actual - ConfigMarkdown.parse() throws Failed to parse YAML frontmatter: incomplete explicit mapping pair. Via parseOption() the error is swallowed and the whole frontmatter block leaks into the body:

data:    {}
content: "---
allowed-tools: Read: every file
description: Review helper
---
Review my code."

Expected:

data:    { "allowed-tools": "Read: every file", "description": "Review helper" }
content: "Review my code."

An underscore key is the control case and works fine - allowed_tools: Read: every file sanitizes correctly. Dotted keys (tools.read:) fail the same way as hyphenated ones.

Impact

Since parseOption() swallows the error and decode still returns a structurally valid object, the failure is silent:

  • Commands (config/plugin/command.ts:71) run with the raw YAML header, --- delimiters and all, pasted into the prompt sent to the model.
  • Agents (config/plugin/agent.ts:154) lose description, mode, model, steps and permissions entirely, and system becomes the raw file.
  • Skills (skill.ts:84) go through the same path.

OS

Windows (platform-independent - pure regex logic)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions