Summary
php artisan boost:update silently unregisters an existing skill when that skill's SKILL.md has invalid YAML in its frontmatter. It removes the entry from boost.json and deletes the agent symlinks (.claude/skills/<name>, .cursor/skills/<name>, .agents/skills/<name>), while reporting Boost guidelines and skills updated successfully.
The skill directory itself stays untouched under .ai/skills/<name>/, so the source of truth is still there — it just becomes invisible to every agent, with no error, no warning, and a success message.
Environment
laravel/boost v2.5.3
- Laravel 13.25.0, PHP 8.4.16
Reproduction
- Create a custom skill with a
description: that is not quoted and contains a colon followed by a space:
---
name: my-skill
description: Does a thing. Covers: the important bit.
---
# My skill
-
Register it: php artisan boost:update (or boost:install), and confirm it appears in boost.json and gets its symlinks. (In my case the skill had been registered for weeks — it worked fine with the agents that read .ai/skills directly.)
-
Run php artisan boost:update again.
Actual:
$ php artisan boost:update
Boost guidelines and skills updated successfully.
$ git status --short
D .agents/skills/my-skill
D .claude/skills/my-skill
D .cursor/skills/my-skill
M boost.json # "my-skill" entry removed
Expected: either the skill keeps working, or the command tells me the frontmatter is invalid and which file/line — anything except removing a registered skill while printing success.
Root cause
The YAML frontmatter above is genuinely invalid (A colon cannot be used in an unquoted mapping value), so the parse failure is fair. The problem is what happens next:
src/Install/SkillComposer.php
protected function parseSkillFrontmatter(string $content): array
{
// ...
try {
return Yaml::parse($matches[1]) ?? [];
} catch (Exception) {
return []; // parse error becomes "no frontmatter"
}
}
and, a few lines above:
$frontmatter = $this->parseSkillFrontmatter($content);
if (empty($frontmatter['name']) || empty($frontmatter['description'])) {
return null; // skill silently drops out of the list
}
The swallowed ParseException is indistinguishable from "this directory has no skill", so the skill is dropped from the collected list, and the stale-cleanup step then removes it from boost.json and deletes the symlinks.
The failure mode is asymmetric in a way that matters: a malformed skill is treated exactly like a deleted one, and the destructive branch is the one that wins.
Why this is easy to miss
A skill that is unregistered but still present on disk keeps looking fine — the directory is there, the file is there, git status shows only deletions of symlinks that are easy to skim past. It stops showing up in boost:list-skills and stops being loaded by the agents, which is exactly the kind of regression nobody notices for weeks. (We only caught it because we have a test asserting that every skill under .ai/skills is registered in boost.json and has its symlinks.)
Suggested fix
Any of these would have prevented it:
- Distinguish parse failure from absence. Let
parseSkillFrontmatter() signal the error (return null, or rethrow) and have the caller warn: Skipping <path>: invalid YAML frontmatter (<message>).
- Never remove on error. Treat a skill that fails to parse as "leave as is" rather than "not present", so cleanup only removes skills whose directories are actually gone.
- At minimum, report it. Print the skipped files at the end of
boost:update instead of the unconditional success message.
Happy to send a PR for (1) + (3) if that's the direction you'd prefer.
Summary
php artisan boost:updatesilently unregisters an existing skill when that skill'sSKILL.mdhas invalid YAML in its frontmatter. It removes the entry fromboost.jsonand deletes the agent symlinks (.claude/skills/<name>,.cursor/skills/<name>,.agents/skills/<name>), while reportingBoost guidelines and skills updated successfully.The skill directory itself stays untouched under
.ai/skills/<name>/, so the source of truth is still there — it just becomes invisible to every agent, with no error, no warning, and a success message.Environment
laravel/boostv2.5.3Reproduction
description:that is not quoted and contains a colon followed by a space:Register it:
php artisan boost:update(orboost:install), and confirm it appears inboost.jsonand gets its symlinks. (In my case the skill had been registered for weeks — it worked fine with the agents that read.ai/skillsdirectly.)Run
php artisan boost:updateagain.Actual:
Expected: either the skill keeps working, or the command tells me the frontmatter is invalid and which file/line — anything except removing a registered skill while printing success.
Root cause
The YAML frontmatter above is genuinely invalid (
A colon cannot be used in an unquoted mapping value), so the parse failure is fair. The problem is what happens next:src/Install/SkillComposer.phpand, a few lines above:
The swallowed
ParseExceptionis indistinguishable from "this directory has no skill", so the skill is dropped from the collected list, and the stale-cleanup step then removes it fromboost.jsonand deletes the symlinks.The failure mode is asymmetric in a way that matters: a malformed skill is treated exactly like a deleted one, and the destructive branch is the one that wins.
Why this is easy to miss
A skill that is unregistered but still present on disk keeps looking fine — the directory is there, the file is there,
git statusshows only deletions of symlinks that are easy to skim past. It stops showing up inboost:list-skillsand stops being loaded by the agents, which is exactly the kind of regression nobody notices for weeks. (We only caught it because we have a test asserting that every skill under.ai/skillsis registered inboost.jsonand has its symlinks.)Suggested fix
Any of these would have prevented it:
parseSkillFrontmatter()signal the error (returnnull, or rethrow) and have the caller warn:Skipping <path>: invalid YAML frontmatter (<message>).boost:updateinstead of the unconditional success message.Happy to send a PR for (1) + (3) if that's the direction you'd prefer.