larksuite/cli · error

%s: %q is not a directory; every %s entry must be a <skill>/

Error message

%s: %q is not a directory; every %s entry must be a <skill>/ dir

What it means

A top-level entry of the skill tree is not a directory; every entry must be a <skill>/ directory. scanSkillTree rejects flat files in the tree root so plugins cannot smuggle non-skill content into the composed FS.

Source

Thrown at internal/skillpolicy/resolver.go:178

	skills map[string]skillManifest
}

// scanSkillTree validates and snapshots a skill tree's top level in one
// pass. The returned set is the only source used by validation and overlay
// composition, so a mutable FS cannot swap unvalidated names between phases.
func scanSkillTree(label string, source fs.FS) (skillTreeSnapshot, error) {
	snapshot := skillTreeSnapshot{source: source, skills: map[string]skillManifest{}}
	if source == nil {
		return snapshot, nil
	}
	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

View on GitHub (pinned to 7fd6ef3c07)

Solutions

  1. Remove the offending file (named in the message) from the embedded tree root
  2. Adjust the embed pattern to include only skill directories
  3. Add a build/test check that lists the FS root and asserts every entry IsDir
  4. Keep non-skill assets in a separate FS outside the skill tree

Example fix

// before
//go:embed overlay/*
var overlayFS embed.FS // includes overlay/NOTES.md
// after
//go:embed overlay
var embedded embed.FS // remove overlay/NOTES.md first
Defensive patterns

Strategy: validation

Validate before calling

entries, _ := fs.ReadDir(skillFS, ".")
for _, e := range entries {
	if !e.IsDir() { return fmt.Errorf("%q must be a directory", e.Name()) }
}

Try / catch

_, err := skillpolicy.ResolveWithReferences(base, specs)
if err != nil && strings.Contains(err.Error(), "is not a directory") { /* remove flat file */ }

Prevention

When it happens

Trigger: ResolveWithReferences with a Base or Overlay FS whose root contains a regular file (README.md, LICENSE, .DS_Store, dotfile) — the FS walks entries and fails on the first non-directory.

Common situations: go:embed patterns that include files alongside skill directories, macOS metadata files committed into embedded assets, or a plugin author adding notes at the overlay root.

Related errors


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