Skip to content

fix(skills): require a line of exactly --- as the SKILL.md frontmatter delimiter - #1136

Open
OsamaAnsar wants to merge 2 commits into
google:mainfrom
OsamaAnsar:fix/skill-md-frontmatter-delimiter
Open

OsamaAnsar wants to merge 2 commits into
google:mainfrom
OsamaAnsar:fix/skill-md-frontmatter-delimiter

Conversation

@OsamaAnsar

@OsamaAnsar OsamaAnsar commented Oct 23, 2026 •

Copy link
Copy Markdown

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

No existing issue found for this (searched open and closed issues and PRs for SKILL.md, frontmatter, BOM, loader.ts).

2. Or, if no issue exists, describe the change:

Problem:
parseFrontmatterYaml in core/src/skills/loader.ts finds the frontmatter with content.startsWith('---') and content.split('---'). Two consequences, both reproduced by calling parseSkillMdContent:

  1. A UTF-8 byte order mark (written by Notepad and by Windows PowerShell 5.1 Out-File/Set-Content -Encoding UTF8; Node's readFile keeps it as a leading U+FEFF) makes the loader throw SKILL.md must start with YAML frontmatter (---), even though the file starts with ---.
  2. Any --- inside the frontmatter ends it. description: Use A --- B for things is silently truncated to Use A, and B for things plus the real closing --- leak into the skill body.

LF and CRLF files without a BOM were already fine.

Solution:
The delimiter in the Agent Skills format is a line of exactly ---. parseFrontmatterYaml now strips a leading U+FEFF, then matches the opening line, the frontmatter and the closing line with one pattern (FRONTMATTER_PATTERN; LF or CRLF, optional trailing spaces/tabs on the delimiter lines). The body is everything after the closing line, trimmed as before. The two existing error messages are unchanged, and an empty --- / --- pair still reports "Invalid YAML in frontmatter" (non-mapping) instead of "not properly closed".

One behaviour change: a file whose only closing --- sits at the end of a value line (description: A test skill ---) used to load with a truncated value and now fails with SKILL.md frontmatter not properly closed with ---, because that is not a delimiter line.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Added 9 test cases to core/test/skills/loader_test.ts (a frontmatter delimiters block in parseSkillMdContent, plus a loadSkillFromDir test that writes a BOM + CRLF SKILL.md to disk).

Before/after, calling parseSkillMdContent on the same inputs:

                           before                                              after
LF                         OK desc="Does a thing"                              OK desc="Does a thing"
CRLF                       OK desc="Does a thing"                              OK desc="Does a thing"
UTF-8 BOM + LF             THROWS SKILL.md must start with YAML frontmatter    OK desc="Does a thing"
UTF-8 BOM + CRLF           THROWS SKILL.md must start with YAML frontmatter    OK desc="Does a thing"
'---' inside description   OK desc="Use A" body="B for things\n---\n# Body"    OK desc="Use A --- B for things" body="# Body"

With only the parser change in loader.ts reverted (typed errors kept), 5 of the new tests fail (both BOM cases, the --- inside a value, the value-line --- case, and the on-disk BOM test); the other new cases pass either way because they pin behaviour that was already correct. With the change restored core/test/skills passes (3 files, 84 passed, 1 skipped), and core/test/skills plus core/test/agents together pass (738 passed, 2 skipped). prettier --check and eslint are clean on the touched files.

I did not run the full unit suite for this change. On my Windows machine two unrelated tests (core/test/utils/file_utils_test.ts, core/test/cli/utils/logs_test.ts) fail with EPERM: operation not permitted, symlink, also on an unmodified main, so I left "All unit tests pass locally" unticked.

Manual End-to-End (E2E) Tests:

Not run against a real agent. I exercised parseSkillMdContent and loadSkillFromDir only.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

Overlap with #1129 (typed errors in the skill loader), which merged after this PR was opened: both changed parseFrontmatterYaml. I merged main into this branch and resolved the one conflict in core/src/skills/loader.ts by keeping both changes. The BOM and exact --- delimiter parsing is this PR, and the two frontmatter checks throw InputValidationError as #1129 introduced. Messages are unchanged, and the error-path tests now also assert the class. Against main, the diff in loader.ts is only the parser change.

For reference, adk-python skills/_utils.py (_parse_skill_md_content) also uses content.startswith("---") and content.split("---", 2), so a --- inside a value ends the frontmatter there too; this fix goes beyond parity with it. I did not test adk-python itself, and did not check how it handles a BOM.

…r delimiter

parseFrontmatterYaml used startsWith('---') and split('---'). A UTF-8 byte
order mark before the opening delimiter made it throw 'SKILL.md must start
with YAML frontmatter (---)', and a '---' inside a value (for example
'description: Use A --- B') ended the frontmatter early, silently
truncating the value and leaking the rest into the body.

Strip a leading U+FEFF and match the opening line, frontmatter and closing
line with a single pattern, accepting LF and CRLF.
@SHAI-krishna-Dobhal

Copy link
Copy Markdown
Contributor

Thanks for the heads-up @OsamaAnsar The changes fit together fine: your parsing fix plus my typed throws. Whichever lands second, I'm happy to do the rebase

Resolve the conflict in core/src/skills/loader.ts with the typed errors
from google#1129: keep the byte order mark and exact --- delimiter parsing, and
throw InputValidationError from the two frontmatter checks. Assert the
error class in the new error-path tests.
@OsamaAnsar

Copy link
Copy Markdown
Author

Thanks @SHAI-KrishnaDobhal, and no rebase needed. #1129 landed first, so I merged main into this branch and resolved the one conflict in parseFrontmatterYaml. The BOM and exact---- parsing stays, and the two frontmatter checks now throw your InputValidationError, messages unchanged. The new error-path tests also assert the class. Your typed throws, cause handling and tests are untouched, and the loader.ts diff against main is just the parser change. Core skills, agents and errors tests pass, and the new tests fail if I revert only the parser.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants