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

  1. Check the underlying cause after 'cannot read root:' for the real fs error
  2. For go:embed, embed the parent directory (//go:embed skills) and use the FS rooted at the skills parent so '.' lists skill dirs
  3. If using fs.Sub, verify the sub-path is the skill-tree root
  4. 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

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


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