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, nilView on GitHub (pinned to 7fd6ef3c07)
Solutions
- Remove the offending file (named in the message) from the embedded tree root
- Adjust the embed pattern to include only skill directories
- Add a build/test check that lists the FS root and asserts every entry IsDir
- 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
- Keep only skill directories at the tree root
- Scope embed patterns to directories
- Gitignore OS metadata files (.DS_Store)
- Add a tree-shape unit test
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
- %s: %q is not a valid skill name
- Invalid column: {column!r}
- Invalid column index: {index}
- anchor outside sheet: {position!r}
- Missing row_count/column_count for sheet {sheet_title(sheet)
AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04).
Data as JSON: /api/errors/309e1b3c15b5cf2e.
Report an issue: GitHub.