siyuan-note/siyuan · error

Obsidian Vault path is unsafe: Vault root is a symbolic link

Error message

Obsidian Vault path is unsafe: Vault root is a symbolic link or reparse point

What it means

A wrapped variant of errObsidianVaultUnsafePath (line 573-574): returned when the Vault root is a symbolic link or a Windows reparse point. The check tests both the Lstat Mode()&os.ModeSymlink flag and isObsidianResolvedLink(abs), which follows links that Lstat alone might miss. The error wraps the unsafe sentinel with ': Vault root is a symbolic link or reparse point'. This prevents symlink-based traversal attacks where a link escapes the intended directory.

Source

Thrown at kernel/model/import_obsidian.go:574

}

func validateObsidianVaultRoot(localPath string) (string, error) {
	if strings.TrimSpace(localPath) == "" {
		return "", fmt.Errorf("%w: path is empty", errObsidianVaultUnreadable)
	}
	abs, err := filepath.Abs(filepath.Clean(localPath))
	if err != nil {
		return "", fmt.Errorf("%w: normalize Vault path: %v", errObsidianVaultUnreadable, err)
	}
	info, err := os.Lstat(abs)
	if err != nil {
		return "", fmt.Errorf("%w: read Vault root: %v", errObsidianVaultUnreadable, err)
	}
	if !info.IsDir() {
		return "", errObsidianVaultNotDirectory
	}
	if info.Mode()&os.ModeSymlink != 0 || isObsidianResolvedLink(abs) {
		return "", fmt.Errorf("%w: Vault root is a symbolic link or reparse point", errObsidianVaultUnsafePath)
	}
	if util.IsSensitivePath(abs) {
		return "", fmt.Errorf("%w: selected Vault path is sensitive", errObsidianVaultUnsafePath)
	}
	workspace, _ := filepath.Abs(filepath.Clean(util.WorkspaceDir))
	if sameObsidianPath(abs, workspace) || gulu.File.IsSubPath(workspace, abs) || gulu.File.IsSubPath(abs, workspace) {
		return "", fmt.Errorf("%w: Vault root and SiYuan workspace contain each other", errObsidianVaultUnsafePath)
	}
	configPath := filepath.Join(abs, ".obsidian")
	configInfo, statErr := os.Lstat(configPath)
	if statErr != nil {
		if os.IsNotExist(statErr) {
			return "", errObsidianVaultConfigMissing
		}
		return "", fmt.Errorf("%w: read Vault config directory: %v", errObsidianVaultUnreadable, statErr)
	}
	if !configInfo.IsDir() || configInfo.Mode()&os.ModeSymlink != 0 || isObsidianResolvedLink(configPath) {
		return "", errObsidianVaultConfigMissing

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Resolve the symlink to its real target with filepath.EvalSymlinks and pass the physical directory.
  2. Replace the symlink/junction with the actual Vault directory, or copy the Vault contents to a non-linked folder.
  3. Select the real underlying folder in the UI picker rather than the link.

Example fix

// before
localPath := "/home/user/vault-link" // symlink
// -> "Obsidian Vault path is unsafe: Vault root is a symbolic link or reparse point"

// after
real, _ := filepath.EvalSymlinks("/home/user/vault-link")
localPath := real // e.g. /mnt/data/MyVault
Defensive patterns

Strategy: validation

Validate before calling

// Resolve symlinks before calling the API
real, err := filepath.EvalSymlinks(localPath)
if err != nil { return err }
info, err := os.Lstat(real)
if err != nil { return err }
if info.Mode()&os.ModeSymlink != 0 { return errors.New("path is still a symlink after resolution") }

Try / catch

if _, err := model.StartObsidianVaultAnalysis(localPath); err != nil {
    if errors.Is(err, errObsidianVaultUnsafePath) && strings.Contains(err.Error(), "symbolic link") {
        return errors.New("the Vault root is a symlink; please select the real folder")
    }
}

Prevention

When it happens

Trigger: POST /api/import/startObsidianVaultAnalysis with a localPath that is a symlink (Unix) or junction/reparse point (Windows), or resolves through isObsidianResolvedLink to a link target. Line 573 fires.

Common situations: User organized Vaults via symlinks; a cloud-storage client created a symlinked folder; on macOS, some folder arrangements under /Volumes use firmlinks; the Vault root is a Windows junction.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/33cb8adb26bfab30. Report an issue: GitHub.