siyuan-note/siyuan · error

Obsidian Vault is unreadable: normalize Vault path: %v

Error message

Obsidian Vault is unreadable: normalize Vault path: %v

What it means

A wrapped variant of errObsidianVaultUnreadable (line 562-564): returned when filepath.Abs(filepath.Clean(localPath)) fails. filepath.Abs can fail on Windows when the current working directory drive cannot be determined, or in edge cases with path separators. The error wraps the unreadable sentinel with ': normalize Vault path: <err>'.

Source

Thrown at kernel/model/import_obsidian.go:564

		ret.ImportAssets[key] = asset
		ret.Analysis.ImportableAssetCount++
		ret.Analysis.ImportableAssetSize += asset.Source.Size
	}
	ret.Analysis.UnreferencedFileCount = len(ret.ImportAssets) - len(ret.ReferencedAssets)
	if ret.Analysis.UnreferencedFileCount < 0 {
		ret.Analysis.UnreferencedFileCount = 0
	}
	progress(100, "Analysis completed")
	return ret, nil
}

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)
	}

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Pass an already-absolute path to avoid reliance on the process working directory.
  2. On Windows, ensure the drive hosting the process CWD is still mounted.
  3. Sanitize the path string to remove NUL bytes or control characters before submitting.
  4. Retry from a stable working directory (e.g. the SiYuan workspace root).

Example fix

// before: relative path with no resolvable CWD
localPath := "./vault" // CWD drive ejected on Windows
// -> "Obsidian Vault is unreadable: normalize Vault path: ..."

// after: pass absolute path
localPath := "D:/Vaults/MyVault"
Defensive patterns

Strategy: validation

Validate before calling

// Pass an absolute path and check it resolves
abs, err := filepath.Abs(filepath.Clean(localPath))
if err != nil { return fmt.Errorf("path cannot be resolved: %w", err) }

Try / catch

if _, err := model.StartObsidianVaultAnalysis(localPath); err != nil {
    if errors.Is(err, errObsidianVaultUnreadable) && strings.Contains(err.Error(), "normalize Vault path") {
        return errors.New("the path could not be resolved to an absolute path; provide a full path")
    }
}

Prevention

When it happens

Trigger: POST /api/import/startObsidianVaultAnalysis with a path that causes filepath.Abs to error — rare, but possible on Windows when the process working directory is on a removed drive, or with paths containing NUL bytes or other characters invalid for the OS path API.

Common situations: Windows: the removable drive holding the CWD was ejected before the call; the path contains embedded NUL or control characters; an exotic OS-level path resolution failure.

Related errors


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