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

  1. Find the offending entry in the skill directory tree and replace the symlink/special file with a regular copy of the file
  2. Remove unrelated linked content from the skill directory
  3. 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

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


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)