larksuite/cli · error

%s: skill %q is missing SKILL.md

Error message

%s: skill %q is missing SKILL.md

What it means

During skill-tree composition, skillpolicy.scanSkillTree probes each skill directory in the base or overlay FS for a SKILL.md manifest. If skillExists reports the file is absent, composition fails with this message labeled by the tree ("host Base", "plugin Base", or "plugin Overlay"). It enforces the invariant that every top-level skill directory ships a SKILL.md.

Source

Thrown at internal/skillpolicy/resolver.go:188

	}
	entries, err := fs.ReadDir(source, ".")
	if err != nil {
		return snapshot, fmt.Errorf("%s: cannot read root: %w", label, err)
	}
	for _, e := range entries {
		name := e.Name()
		if !e.IsDir() {
			return snapshot, fmt.Errorf("%s: %q is not a directory; every %s entry must be a <skill>/ dir", label, name, label)
		}
		if !isSkillName(name) {
			return snapshot, fmt.Errorf("%s: %q is not a valid skill name", label, name)
		}
		ok, err := skillExists(source, name)
		if err != nil {
			return snapshot, fmt.Errorf("%s: probing skill %q: %w", label, name, err)
		}
		if !ok {
			return snapshot, fmt.Errorf("%s: skill %q is missing SKILL.md", label, name)
		}
		manifest, err := readSkillManifest(source, name)
		if err != nil {
			return snapshot, fmt.Errorf("%s: skill %q has invalid metadata: %w", label, name, err)
		}
		snapshot.skills[name] = manifest
	}
	return snapshot, nil
}

// validateSelection rejects allow/remove entries that cannot compose against
// the already-validated base snapshot.
func validateSelection(lower skillTreeSnapshot, spec *platform.SkillsOverlay) error {
	if err := validateSkillNames("Allow", spec.Allow); err != nil {
		return err
	}
	if err := validateSkillNames("Remove", spec.Remove); err != nil {
		return err

View on GitHub (pinned to 7fd6ef3c07)

Solutions

  1. Add a valid SKILL.md to the skill directory named in the message inside the offending FS (label tells you whether it is the host base, plugin Base, or plugin Overlay).
  2. Remove the empty/partial skill directory from the overlay if it was committed accidentally.
  3. If you are a wrapper integrator, verify the base passed to ResolveWithReferences (via cmd.SetEmbeddedSkillContent) is the complete embedded skill tree.
  4. Check fs.FS path casing/subdirectory nesting; embedded trees commonly need io/fs.Sub so <name>/SKILL.md exists at the expected path.

Example fix

// before (overlay embed misses manifest)
//go:embed myskill/references
var overlayFS embed.FS
// after
//go:embed myskill/SKILL.md myskill/references
var overlayFS embed.FS
Defensive patterns

Strategy: validation

Validate before calling

func hasSkillManifest(fsys fs.FS, name string) error {
    _, err := fs.Stat(fsys, name+"/SKILL.md")
    if err != nil {
        return fmt.Errorf("skill %q is missing SKILL.md", name)
    }
    return nil
}

Try / catch

resolved, err := skillpolicy.ResolveWithReferences(base, specs)
if err != nil {
    if strings.Contains(err.Error(), "missing SKILL.md") {
        // surface the labeled tree and skill name from the message
    }
    return err
}

Prevention

When it happens

Trigger: Calling skillpolicy.ResolveWithReferences with a base fs.FS or a SkillsOverlay whose Base/Overlay FS contains a skill directory lacking SKILL.md at <name>/SKILL.md.

Common situations: Plugin authors build an embed.FS with only the skill's reference/asset files and forget SKILL.md; a packaging script strips markdown files; the skill directory name and file layout disagree after a rename.

Related errors


AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04). Data as JSON: /api/errors/2b4617aa54000173. Report an issue: GitHub.