siyuan-note/siyuan · error

: normalize Vault path: (wrapped: Obsidian Vault is…

Error message

%w: normalize Vault path: %v (wrapped: Obsidian Vault is unreadable)

What it means

validateObsidianVaultRoot wraps errObsidianVaultUnreadable with 'normalize Vault path: %v' when filepath.Abs(filepath.Clean(localPath)) fails. filepath.Abs essentially only fails when resolving the working directory fails (e.g. the process cwd was deleted), so this indicates a broken process environment rather than a user mistake.

Solutions

  1. Restart the SiYuan kernel from a valid existing working directory
  2. Pass an absolute vault path instead of a relative one so Abs has less to resolve
  3. Check the mount/disk holding the kernel's cwd and restore it

Example fix

// before
analyzeVault({ localPath: '../vaults/notes' });
// after
analyzeVault({ localPath: '/home/user/Documents/vaults/notes' }); // absolute path
Defensive patterns

Strategy: fallback

Validate before calling

try { process.cwd(); } catch { alert('Working directory is gone; restart the kernel'); }

Try / catch

try { await analyzeVault(opts); } catch (e) { if (isVaultUnreadable(e) && /normalize Vault path/.test(String(e))) restartKernelThenRetry(); else throw e; }

Prevention

When it happens

Trigger: Calling the analyze/import API when the kernel process's current working directory has been removed, making Getwd (used by filepath.Abs for relative paths) fail.

Common situations: Kernel launched from a directory that was deleted or unmounted afterwards; containers with volatile workdirs; running on a removed network share.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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

Appendix: 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 9f775e8a12)