{"record":{"id":"05ba2f7bdb2e029c","repo":"larksuite/cli","slug":"s-cannot-read-root-w","errorCode":null,"errorMessage":"%s: cannot read root: %w","messagePattern":"(.+?): cannot read root: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/skillpolicy/resolver.go","lineNumber":173,"sourceCode":"\treturn owners\n}\n\ntype skillTreeSnapshot struct {\n\tsource fs.FS\n\tskills map[string]skillManifest\n}\n\n// scanSkillTree validates and snapshots a skill tree's top level in one\n// pass. The returned set is the only source used by validation and overlay\n// composition, so a mutable FS cannot swap unvalidated names between phases.\nfunc scanSkillTree(label string, source fs.FS) (skillTreeSnapshot, error) {\n\tsnapshot := skillTreeSnapshot{source: source, skills: map[string]skillManifest{}}\n\tif source == nil {\n\t\treturn snapshot, nil\n\t}\n\tentries, err := fs.ReadDir(source, \".\")\n\tif err != nil {\n\t\treturn snapshot, fmt.Errorf(\"%s: cannot read root: %w\", label, err)\n\t}\n\tfor _, e := range entries {\n\t\tname := e.Name()\n\t\tif !e.IsDir() {\n\t\t\treturn snapshot, fmt.Errorf(\"%s: %q is not a directory; every %s entry must be a <skill>/ dir\", label, name, label)\n\t\t}\n\t\tif !isSkillName(name) {\n\t\t\treturn snapshot, fmt.Errorf(\"%s: %q is not a valid skill name\", label, name)\n\t\t}\n\t\tok, err := skillExists(source, name)\n\t\tif err != nil {\n\t\t\treturn snapshot, fmt.Errorf(\"%s: probing skill %q: %w\", label, name, err)\n\t\t}\n\t\tif !ok {\n\t\t\treturn snapshot, fmt.Errorf(\"%s: skill %q is missing SKILL.md\", label, name)\n\t\t}\n\t\tmanifest, err := readSkillManifest(source, name)\n\t\tif err != nil {","sourceCodeStart":155,"sourceCodeEnd":191,"githubUrl":"https://github.com/larksuite/cli/blob/7fd6ef3c07182257ce776cdc5a614e122d5bd4b3/internal/skillpolicy/resolver.go#L155-L191","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n//go:embed skills/*.md\nvar content embed.FS // '.' has no skill dirs\n// after\n//go:embed skills\nvar embedded embed.FS\ncontent, _ := fs.Sub(embedded, \"skills\")","handlingStrategy":"validation","validationCode":"if _, err := fs.ReadDir(skillFS, \".\"); err != nil {\n\treturn fmt.Errorf(\"skill FS root unreadable: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"res, err := skillpolicy.ResolveWithReferences(base, specs)\nif errors.Is(err, skillpolicy.ErrInvalidHostBase) { /* fix host embed */ }","preventionTips":["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"],"tags":["go","fs","embed","io"],"backgroundTag":"unreadable-fs-root","analyzedSha":"7fd6ef3c07182257ce776cdc5a614e122d5bd4b3","analyzedAt":"2026-09-04T21:17:44.649Z","contentChangedAt":"2026-09-04T21:17:44.649Z","schemaVersion":2},"datasetVersion":"2026-09-12T02:17:10.037Z"}