Skip to content

Skills with nested metadata maps are silently dropped during discovery #3331

Description

@rmay1er

title: "Skills with nested metadata maps are silently dropped during discovery"
labels: []

Summary

A skill whose YAML frontmatter metadata field contains a nested map (a value
that is a map rather than a scalar string) fails to parse and is silently
dropped from discovery. The only signal is a WARN line in crush.log; the
skill never appears in <available_skills>, and there is no in-app indication
of why.

The Agent Skills spec defines metadata as map[string]string, but real-world
skill packs embed nested objects under metadata. The most widely used Go skill
pack, samber/cc-skills-golang,
does this for every one of its skills. The official reference parser
skills-ref
tolerates it by coercing values to strings; Crush instead hard-fails the entire
file and discards an otherwise valid skill.

Environment

  • Crush: v0.85.0 (Homebrew, darwin/arm64)
  • Skills source: samber/cc-skills-golang installed under
    ~/.config/crush/skills/
  • Config: "skills_paths": ["~/.config/crush/skills"]

Reproduction

  1. Install any skill whose frontmatter contains a nested metadata map. A
    real example from samber/cc-skills-golang/golang-testing/SKILL.md
    (abbreviated):

    ---
    name: golang-testing
    description: "Production-ready Golang tests ..."
    user-invocable: true
    license: MIT
    compatibility: Designed for Claude Code or similar AI coding agents ...
    metadata:
      author: samber
      version: "1.2.3"
      openclaw:
        emoji: "🧪"
        homepage: https://github.com/samber/cc-skills-golang
    allowed-tools: Read Edit Write Glob Grep Bash(go:*)
    ---
  2. Place it under a configured skills path, e.g.
    ~/.config/crush/skills/golang-testing/SKILL.md.

  3. Start Crush. The skill is absent from <available_skills>.

  4. Confirm via the log:

    grep "Failed to parse skill file" ~/.crush/logs/crush.log

Actual behavior

The skill is not loaded. crush.log contains one WARN per affected file:

WARN ... "Failed to parse skill file"
  path: .../golang-testing/SKILL.md
  error: parsing frontmatter: yaml: unmarshal errors:
    line 10: cannot unmarshal !!map into string

With the full samber/cc-skills-golang pack installed, 0 of 30 parseable
skills appear in <available_skills>. (The remaining 16 entries in that pack
are broken symlinks pointing at missing targets on disk — a separate,
unrelated issue.)

Expected behavior

Either:

  • (A) Tolerant parsing. Coerce non-string metadata values to their
    string representation, mirroring the reference skills-ref parser
    (skills-ref/src/skills_ref/parser.py):

    if "metadata" in metadata and isinstance(metadata["metadata"], dict):
        metadata["metadata"] = {str(k): str(v) for k, v in metadata["metadata"].items()}

    This keeps the spec's map[string]string contract in Crush's Skill struct
    intact while no longer discarding an entire skill over one non-conformant
    metadata value. name, description, compatibility, etc. stay strictly
    typed; only metadata becomes lossy — which is acceptable, since metadata
    is explicitly the spec's escape hatch for client-specific data.

  • (B) Strict, but loud. Keep rejecting nested maps, but surface the
    failure to the user in the UI (sidebar / landing-page skill count) instead of
    only in crush.log, so a missing skill is diagnosable without grepping logs.

Root cause

internal/skills/skills.go:

  • Line 45: Metadata map[string]string \yaml:"metadata,omitempty"``
  • Line 174: yaml.Unmarshal([]byte(frontmatter), &skill) inside ParseContent

gopkg.in/yaml.v3 cannot unmarshal a YAML !!map into a Go string. When
metadata contains a nested object, unmarshalling the entire Skill fails,
ParseContent returns an error, and DiscoverWithStates logs a WARN and
moves on — the skill never enters the returned skills slice and therefore
never reaches <available_skills>.

Why I lean toward (A)

  1. The spec's metadata field exists precisely for client-specific extensions.
    From the specification: "Clients can
    use this to store additional properties not defined by the Agent Skills
    spec." A nested openclaw: block is exactly such a property.
  2. The official reference parser already tolerates it via str(v) coercion.
    Crush is currently stricter than the spec's own reference implementation.
  3. The blast radius is real. samber/cc-skills-golang is widely used, and today
    every user who installs it silently loses all Go skills in Crush with no
    on-screen hint and no actionable error.
  4. The cost of coercion is low. metadata is never used for control flow in
    Crush — it is opaque bag-of-strings data. Stringifying nested values cannot
    regress behavior; it only stops dropping skills.

Related

Notes

This is filed as a diagnosis and a proposed direction, not a finished fix. I can
follow up with a PR implementing (A) — a small change in ParseContent plus
tests — if that direction looks right to the maintainers, noting that (A)
loosens strict spec conformance slightly in exchange for no longer dropping
popular real-world skills.


Investigated and reported by Crush running model glm-5.2 via OpenRouter.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions