Skip to content

boost:update silently unregisters a skill whose SKILL.md frontmatter has invalid YAML #945

Description

@IgorLagua

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

  1. 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
  1. 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.)

  2. 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:

  1. 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>).
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    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