Fixed skills failing to load: quote SKILL.md frontmatter in every channel, warn on over-long description/name (0.7.1) - #11
Conversation
… channel and warning on over-long description or name, released as 0.7.1
There was a problem hiding this comment.
Pull request overview
This PR fixes cross-adapter skill loading failures by ensuring SKILL.md frontmatter is consistently normalized/quoted on the single shared copy path, and adds sync-time warnings for frontmatter fields that will be rejected by downstream skill loaders.
Changes:
- Move
SKILL.mdfrontmatter quoting intocopy_skill_bundleand remove the redundant quoting pass insync_open_skill_dirs. - Extend
lint_frontmatterto warn whendescription(>1024 chars) orname(>64 chars) exceed known loader limits. - Bump sync version to
0.7.1, update docs/changelog, and add CI coverage to prevent regressions.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| intelligence/sync/scripts/VERSION | Bumps engine version to 0.7.1. |
| intelligence/sync/scripts/lib/common.sh | Centralizes SKILL frontmatter quoting in copy_skill_bundle; adds length warnings in lint_frontmatter; removes redundant pass in open-skill sync. |
| intelligence/sync/INIT.md | Updates embedded sync_version to 0.7.1. |
| intelligence/sync/docs/CONVENTIONS.md | Documents hard limits that cause skills to be rejected/disappear. |
| intelligence/sync/docs/ADAPTERS.md | Documents that adapters must route skill copies through copy_skill_bundle and why. |
| examples/with-remote-skills/config.yaml | Updates example sync_version to 0.7.1. |
| examples/platform-with-submodules/config.yaml | Updates example sync_version to 0.7.1. |
| examples/go-api/config.yaml | Updates example sync_version to 0.7.1. |
| examples/go-api-with-pi-and-codex/config.yaml | Updates example sync_version to 0.7.1. |
| examples/go-api-with-opencode/config.yaml | Updates example sync_version to 0.7.1. |
| examples/dotnet-api-with-react-frontend/config.yaml | Updates example sync_version to 0.7.1. |
| docs/CONVENTIONS.md | Mirrors conventions update in top-level docs. |
| docs/ADAPTERS.md | Mirrors adapters update in top-level docs. |
| CHANGELOG.md | Adds 0.7.1 release notes describing the fix and new warnings. |
| .github/workflows/ci.yml | Adds fixture + assertion ensuring argument-hint is quoted in all emitted channels. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if [ -f "$dest_dir/SKILL.md" ]; then | ||
| copy_md_with_quoted_frontmatter "$dest_dir/SKILL.md" "$dest_dir/SKILL.md.tmp-q" | ||
| mv "$dest_dir/SKILL.md.tmp-q" "$dest_dir/SKILL.md" | ||
| fi |
There was a problem hiding this comment.
Correct, and thank you — that was a real regression I introduced. [ -f ] follows the link, so the quoting pass would have read the symlink's target and written that content into a real file, turning skills/x/SKILL.md -> /etc/… into a copy of a host file inside .claude/skills/. Exactly the leak cp -R is used to prevent.
Fixed: a symlinked SKILL.md is now left precisely as copied (-L checked before -f), with a WARN saying it is emitted unprocessed — no quoting, no token expansion. The find … -type f loop already skipped symlinks, so that path was safe; this was the one that followed them.
Verified on Linux with a hostile fixture (SKILL.md -> /tmp/secret/passwd containing a marker string): the marker appears in no generated output, .claude/skills/evil-skill/SKILL.md is still a symlink rather than a materialized file, and a normal sibling skill still gets its argument-hint quoted. Added as a CI guardrail step so the guarantee has a test rather than a comment.
…reported as a failure
… through it and copy a host file into the outputs
Summary
Skills were failing to load in Claude Code, Cursor and Copilot, with two different errors and one shared cause: the engine only quoted
SKILL.mdfrontmatter on its way into the Agent Skills open-standard directory (.agents/skills/). Every other channel got the source frontmatter verbatim — soargument-hint: [route-name], which YAML reads as a flow sequence, arrived as a list and Claude Code rejected the whole skill ("argument-hint must be a string"). It does not degrade; the skill simply vanishes from the picker. The second error ("Skill description must be at most 1024 characters") is an authoring problem the engine had no opinion about at all — it synced the file happily and the tool rejected it later, with nothing in the sync output pointing at the cause.Non-breaking: no migration, no schema change. Patch release
0.7.1.Type of change
What changed
argument-hint must be a stringin.claude/skills/,.cursor/skills/,.github/skills/sync_open_skill_dirs(the.agents/skills/path);copy_skill_bundle— the copy path every other adapter uses — did not quotecopy_skill_bundle, so it is applied once, on the single path all adapters share. The now-redundant second pass insync_open_skill_dirsis removed. Idempotent: already-quoted values pass through untouched.Skill description must be at most 1024 characterslint_frontmatternow warns at sync time with file, line and actual length — fordescription(>1024) andname(>64). Stays a warning: the engine reports, the author shortens the text.Verification
bash intelligence/sync/scripts/sync.shruns cleanly (IS_STATUS=ok) against fiveexamples/*(on Linux via WSL)lint_frontmatterproduces no new warnings on valid input; the new length warning fires exactly on the over-long fixtureshellcheck --severity=warningcleanVERSION/INIT.md/ all six examples in lockstep at0.7.1docs/CONVENTIONS.md— a table of the limits that reject a skill outright;docs/ADAPTERS.md— adapters must copy skills throughcopy_skill_bundle;CHANGELOG.md)Reproduced both reported errors as fixtures first (an unquoted list-shaped
argument-hint, and a 1480-chardescription), then confirmed:argument-hint: "[suite-name]"— a string — in all four channels (.claude,.cursor,.github,.agents); parsed back with a YAML reader to confirm the type, not just the textWARN: …/SKILL.md:3 description is 1480 chars — over the 1024-char limit; the skill/agent will be REJECTED at load timereferences/) still ship — the quoting pass sits inside the bundle copy and does not disturb itNotes for reviewers
argument-hint: [route-name], and CI asserts every channel emits it quoted. Without that, this regresses silently — the failure only ever appears inside someone's IDE, never in sync output..claude/.cursor/.githubskills now carry quoteddescription/argument-hintwhere they previously carried the raw value. That is the fix, and it is what.agents/skills/has always emitted.