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
-
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:*)
---
-
Place it under a configured skills path, e.g.
~/.config/crush/skills/golang-testing/SKILL.md.
-
Start Crush. The skill is absent from <available_skills>.
-
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)
- 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.
- The official reference parser already tolerates it via
str(v) coercion.
Crush is currently stricter than the spec's own reference implementation.
- 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.
- 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.
title: "Skills with nested
metadatamaps are silently dropped during discovery"labels: []
Summary
A skill whose YAML frontmatter
metadatafield contains a nested map (a valuethat is a map rather than a scalar string) fails to parse and is silently
dropped from discovery. The only signal is a
WARNline incrush.log; theskill never appears in
<available_skills>, and there is no in-app indicationof why.
The Agent Skills spec defines
metadataasmap[string]string, but real-worldskill packs embed nested objects under
metadata. The most widely used Go skillpack,
samber/cc-skills-golang,does this for every one of its skills. The official reference parser
skills-reftolerates it by coercing values to strings; Crush instead hard-fails the entire
file and discards an otherwise valid skill.
Environment
v0.85.0(Homebrew, darwin/arm64)samber/cc-skills-golanginstalled under~/.config/crush/skills/"skills_paths": ["~/.config/crush/skills"]Reproduction
Install any skill whose frontmatter contains a nested
metadatamap. Areal example from
samber/cc-skills-golang/golang-testing/SKILL.md(abbreviated):
Place it under a configured skills path, e.g.
~/.config/crush/skills/golang-testing/SKILL.md.Start Crush. The skill is absent from
<available_skills>.Confirm via the log:
Actual behavior
The skill is not loaded.
crush.logcontains oneWARNper affected file:With the full
samber/cc-skills-golangpack installed, 0 of 30 parseableskills appear in
<available_skills>. (The remaining 16 entries in that packare broken symlinks pointing at missing targets on disk — a separate,
unrelated issue.)
Expected behavior
Either:
(A) Tolerant parsing. Coerce non-string
metadatavalues to theirstring representation, mirroring the reference
skills-refparser(
skills-ref/src/skills_ref/parser.py):This keeps the spec's
map[string]stringcontract in Crush'sSkillstructintact while no longer discarding an entire skill over one non-conformant
metadata value.
name,description,compatibility, etc. stay strictlytyped; only
metadatabecomes lossy — which is acceptable, sincemetadatais 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:Metadata map[string]string \yaml:"metadata,omitempty"``yaml.Unmarshal([]byte(frontmatter), &skill)insideParseContentgopkg.in/yaml.v3cannot unmarshal a YAML!!mapinto a Gostring. Whenmetadatacontains a nested object, unmarshalling the entireSkillfails,ParseContentreturns an error, andDiscoverWithStateslogs aWARNandmoves on — the skill never enters the returned
skillsslice and thereforenever reaches
<available_skills>.Why I lean toward (A)
metadatafield 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.str(v)coercion.Crush is currently stricter than the spec's own reference implementation.
samber/cc-skills-golangis widely used, and todayevery user who installs it silently loses all Go skills in Crush with no
on-screen hint and no actionable error.
metadatais never used for control flow inCrush — it is opaque bag-of-strings data. Stringifying nested values cannot
regress behavior; it only stops dropping skills.
Related
WARNline upto 10× per
crush run. Compounds this bug: one non-conformantmetadatablock produces a flood of warnings. Fixing tolerance here would also cut that
noise at the source.
the reporter clarified they were treating skills as commands. Not the same
bug, but the shared symptom ("installed skill not visible") is worth linking.
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
ParseContentplustests — 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.2via OpenRouter.