larksuite/cli · error
%s: cannot read root: %w
Error message
%s: cannot read root: %w
What it means
scanSkillTree could not read the root directory ('.') of the labeled skill tree ('host Base', 'plugin Base', or 'plugin Overlay'). For a host base this is wrapped in ErrInvalidHostBase; for plugin-owned trees it appears as 'plugin %q skill spec'. The FS is present but its root is unreadable.
Source
Thrown at internal/skillpolicy/resolver.go:173
return owners
}
type skillTreeSnapshot struct {
source fs.FS
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 {View on GitHub (pinned to 7fd6ef3c07)
Solutions
- Check the underlying cause after 'cannot read root:' for the real fs error
- For go:embed, embed the parent directory (//go:embed skills) and use the FS rooted at the skills parent so '.' lists skill dirs
- If using fs.Sub, verify the sub-path is the skill-tree root
- Test the FS standalone with fs.ReadDir(fsys, ".") before passing it to the resolver
Example fix
// before //go:embed skills/*.md var content embed.FS // '.' has no skill dirs // after //go:embed skills var embedded embed.FS content, _ := fs.Sub(embedded, "skills")
Defensive patterns
Strategy: validation
Validate before calling
if _, err := fs.ReadDir(skillFS, "."); err != nil {
return fmt.Errorf("skill FS root unreadable: %w", err)
} Try / catch
res, err := skillpolicy.ResolveWithReferences(base, specs)
if errors.Is(err, skillpolicy.ErrInvalidHostBase) { /* fix host embed */ } Prevention
- Embed the skill-tree parent directory so '.' enumerates skill dirs
- Verify with fs.ReadDir(fsys, ".") in tests before wiring
- Use fs.Sub deliberately and confirm the sub-root
- Prefer embed.FS over custom FS implementations
When it happens
Trigger: fs.ReadDir(source, ".") fails inside ResolveWithReferences: the embed.FS root is wrong, an http/vfs-backed FS errors, or a custom fs.FS implementation rejects the '.' path.
Common situations: Embedding the wrong directory (e.g. embedding 'skills/*' files instead of the parent dir so '.' lists nothing readable), a custom FS that does not implement ReadDirFS semantics, or an I/O/permission fault on a disk-backed FS.
Related errors
- %s: probing skill %q: %w
- invalid chart size: {size!r}
- build embeds no base skill content
- host embedded skill content is invalid
- %s: %q is not a directory; every %s entry must be a <skill>/
AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04).
Data as JSON: /api/errors/05ba2f7bdb2e029c.
Report an issue: GitHub.