siyuan-note/siyuan · error

staged document tree contains a symbolic link

Error message

staged document tree contains a symbolic link

What it means

While copying the staged temporary document tree into the target notebook, the copier walks each entry and explicitly rejects symbolic links. Symlinks in the staged tree could escape the destination root or break on export, so the copy aborts with this error.

Source

Thrown at kernel/model/import_obsidian.go:2415

func copyObsidianTreeFiles(sourceRoot, destinationRoot string) error {
	return filepath.WalkDir(sourceRoot, func(current string, entry fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		rel, err := filepath.Rel(sourceRoot, current)
		if err != nil {
			return err
		}
		if rel == "." {
			return nil
		}
		destination := filepath.Join(destinationRoot, rel)
		if entry.IsDir() {
			return os.MkdirAll(destination, 0755)
		}
		if entry.Type()&os.ModeSymlink != 0 {
			return errors.New("staged document tree contains a symbolic link")
		}
		return filelock.Copy(current, destination)
	})
}

func availableObsidianNotebookName(requested string) string {
	base := sanitizeObsidianTitle(requested)
	existing := map[string]bool{}
	boxes, _ := ListNotebooks()
	for _, box := range boxes {
		existing[strings.ToLower(box.Name)] = true
	}
	if !existing[strings.ToLower(base)] {
		return base
	}
	for index := 2; ; index++ {
		candidate := fmt.Sprintf("%s (%d)", base, index)
		if !existing[strings.ToLower(candidate)] {

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Inspect the staging directory for symlinks (find -type l) and remove them, then retry the import
  2. Re-run the import with a clean temporary directory so stale links are not copied
  3. Ensure no tool between staging and copy replaces files with symlinks (sync/backup/link farms)
  4. If symlinks are intentional, replace them with real file copies before import

Example fix

// before: symlink present in staging dir gets rejected
// staged/assets/logo.png -> /etc/passwd (symlink)
copyTree(stagedDir, destinationRoot) // errors
// after: dereference real copies before staging
filepath.Walk(vaultAssets, func(p string, info os.FileInfo, err error) error {
	if info.Mode()&os.ModeSymlink != 0 {
		target, _ := os.Readlink(p)
		return os.Remove(p) // or copy target contents over the link
	}
	return nil
})
Defensive patterns

Strategy: validation

Validate before calling

err := filepath.WalkDir(stagedDir, func(p string, d fs.DirEntry, err error) error {
	if err == nil && d.Type()&os.ModeSymlink != 0 {
		return fmt.Errorf("symlink in staging: %s", p)
	}
	return err
})

Type guard

func isSymlink(d fs.DirEntry) bool { return d.Type()&os.ModeSymlink != 0 }

Try / catch

if err := copyStagedTree(src, dst); err != nil && strings.Contains(err.Error(), "symbolic link") {
	// clean staging dir, remove links, retry
}

Prevention

When it happens

Trigger: filepath.WalkDir over the staged docs temp directory encounters a file entry whose Type() has os.ModeSymlink set — i.e. someone or something placed a symlink into the staging directory before/during the copy to destinationRoot.

Common situations: A malicious or buggy step (or third-party tool) created symlinks in the temp staging directory; a user pre-seeded the temp directory with symlinked assets; importing on a filesystem that materializes links (some cloud mounts); a previous interrupted run left links behind.

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@8641553a1f (2026-09-11). Data as JSON: /api/errors/55f376f4c9565a09. Report an issue: GitHub.