siyuan-note/siyuan · error

: read Vault root: (wrapped: Obsidian Vault is unreadable)

Error message

%w: read Vault root: %v (wrapped: Obsidian Vault is unreadable)

What it means

validateObsidianVaultRoot wraps errObsidianVaultUnreadable with 'read Vault root: %v' when os.Lstat(abs) fails — the path does not exist or cannot be stat'ed (permission, IO error). The underlying OS error is included in the message.

Solutions

  1. Verify the path exists (ls / dir) and correct typos, then re-run the analysis
  2. Remount the drive/share hosting the vault
  3. Fix permissions: chmod/chown, or grant the app Full Disk/folder access (macOS System Settings)
  4. Pick the vault folder again via the folder picker to store a fresh path

Example fix

// before
analyzeVault({ localPath: '/Volumes/USB/vault' }); // drive unplugged
// after
if (fs.existsSync(vaultPath)) {
  analyzeVault({ localPath: vaultPath });
}
Defensive patterns

Strategy: validation

Validate before calling

const st = await fs.promises.stat(vaultPath).catch(() => null);
if (!st || !st.isDirectory()) throw new Error(`Vault path missing: ${vaultPath}`);

Type guard

async function isReadableDir(p) { try { return (await fs.promises.stat(p)).isDirectory(); } catch { return false; } }

Try / catch

try { await analyzeVault(opts); } catch (e) { if (isVaultUnreadable(e) && /read Vault root/.test(String(e))) { showPathFixUi(opts.localPath); } else throw e; }

Prevention

When it happens

Trigger: Analyzing a vault path that was deleted/renamed, contains a typo, sits on an unmounted drive, or is unreadable due to permissions (including macOS TCC blocking access to folders).

Common situations: Vault moved since last use; network drive disconnected; Windows OneDrive folder offline; macOS 'Files and Folders' permission denied for the kernel app.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/68bed470ad0c05f4. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/import_obsidian.go:568

	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)
	}
	configPath := filepath.Join(abs, ".obsidian")
	configInfo, statErr := os.Lstat(configPath)
	if statErr != nil {
		if os.IsNotExist(statErr) {

View on GitHub (pinned to 9f775e8a12)