siyuan-note/siyuan · error
skill directory contains a link or special file
Error message
skill directory contains a link or special file
What it means
managedSkillRevision computes a content hash of a skill directory used as an optimistic-concurrency revision. While walking the tree it refuses to hash any entry that is a symlink or a non-regular, non-directory file, producing this error. This guarantees revisions only cover real skill content and blocks link/special-file escapes inside skill directories.
Solutions
- Find the offending entry in the skill directory tree and replace the symlink/special file with a regular copy of the file
- Remove unrelated linked content from the skill directory
- Retry the operation once the tree contains only regular files and directories
Example fix
// before ln -s ../../assets/prompt.txt my-skill/assets/prompt.txt // after cp ../../assets/prompt.txt my-skill/assets/prompt.txt
Defensive patterns
Strategy: validation
Validate before calling
func treeHasLinks(root *os.Root, dir string) error {
return fs.WalkDir(root.FS(), dir, func(p string, d fs.DirEntry, err error) error {
if err != nil { return err }
info, err := d.Info()
if err != nil { return err }
if info.Mode()&os.ModeSymlink != 0 || (!info.IsDir() && !info.Mode().IsRegular()) {
return fmt.Errorf("link/special file at %s", p)
}
return nil
})
} Type guard
func isPlainEntry(d fs.DirEntry) bool {
return d.IsDir() || d.Type().IsRegular()
} Try / catch
if err := ManageSkillFiles(req); err != nil && strings.Contains(err.Error(), "link or special file") {
// walk the skill dir and replace links with copies
} Prevention
- Copy, don't link, shared assets into skill directories
- Run link-scanning before zipping/unzipping or syncing skill folders
- Exclude node_modules-like trees from skill directories
When it happens
Trigger: Calling ManageSkillFiles read/write/move/remove on an existing path whose directory tree contains a symlink, FIFO, socket or device file; the hash walk (fs.WalkDir + entry.Info) hits that entry. Also surfaces indirectly via writeManagedSkillSource's re-validation before a replace rename.
Common situations: Users symlink asset or script files into a skill folder to avoid duplication; a sync tool materialized junctions/links; a node_modules-style tree with symlinks was copied into the skill directory.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- skill path is not a regular file or directory
- asset path contains an unresolved symbolic link
- asset path resolves outside notebook assets directory
- attribute view definition is not a regular file
- encrypted document is a symbolic link
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/28d0051bdcd8fca1.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/util/skill_manage.go:138
}
func isManagedSkillManifest(p string) bool {
return strings.Count(p, "/") == 1 && strings.EqualFold(path.Base(p), "SKILL.md")
}
// 目录版本包含每个资源的内容哈希,保持大小和修改时间不变的外部编辑也会产生冲突。
func managedSkillRevision(root *os.Root, p string) (string, error) {
h := sha256.New()
err := fs.WalkDir(root.FS(), p, func(name string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
info, err := entry.Info()
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink != 0 || (!info.IsDir() && !info.Mode().IsRegular()) {
return errors.New("skill directory contains a link or special file")
}
if info.IsDir() {
fmt.Fprintf(h, "directory\x00%s\x00", name)
return nil
}
file, err := root.Open(name)
if err != nil {
return err
}
hash := sha256.New()
_, copyErr := io.Copy(hash, file)
closeErr := file.Close()
if copyErr != nil {
return copyErr
}
if closeErr != nil {
return closeErr
}View on GitHub (pinned to 9f775e8a12)