siyuan-note/siyuan · error

Obsidian Vault is unreadable: read Vault config directory: %

Error message

Obsidian Vault is unreadable: read Vault config directory: %v

What it means

Thrown by validateObsidianVaultRoot when os.Lstat on the vault's .obsidian config directory fails with an error that is NOT 'not exist' (e.g. permission denied, I/O error, stale NFS handle). It wraps the sentinel errObsidianVaultUnreadable, which the task layer maps to user-facing language code 336 ('Cannot read the selected Obsidian Vault'). The existence-vs-missing distinction matters: a truly absent .obsidian yields errObsidianVaultConfigMissing (339), whereas an existing-but-inaccessible one yields this error.

Source

Thrown at kernel/model/import_obsidian.go:589

		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
	}
	return abs, nil
}

func scanObsidianVaultFiles(ctx context.Context, vault *obsidianVaultContext, relDir, absDir string) error {
	if err := ctx.Err(); err != nil {
		return err
	}
	entries, err := os.ReadDir(absDir)
	if err != nil {
		return fmt.Errorf("read Vault directory [%s]: %w", relDir, err)
	}
	sort.Slice(entries, func(i, j int) bool { return strings.ToLower(entries[i].Name()) < strings.ToLower(entries[j].Name()) })
	for _, entry := range entries {
		if err = ctx.Err(); err != nil {

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Verify the SiYuan process can stat <vault>/.obsidian: run ls -la <vault>/.obsidian from the same user context; on failure fix ownership/permissions (chmod/chown) so the path is traversable.
  2. If the vault lives on a network/removable volume, remount or copy it to a local directory and re-run StartObsidianVaultAnalysis pointing at the local copy.
  3. On Windows, disable or whitelist the vault in antivirus/EDR that may hold an exclusive handle on .obsidian.
  4. Confirm .obsidian is actually a normal directory (not a broken junction/symlink) — a symlinked or reparse-point .obsidian is rejected as errObsidianVaultConfigMissing and must be made a real directory.

Example fix

// before: vault on a read-only / locked network share
StartObsidianVaultAnalysis("/mnt/nfs/vault")  // Lstat(.obsidian) -> EACCES

// after: copy to a local, fully-owned path
cp -r /mnt/nfs/vault ~/vault && chmod -R u+rwX ~/vault
StartObsidianVaultAnalysis(filepath.Join(home, "vault"))
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight: ensure <vault>/.obsidian is stat-able by this process before analysis.
func vaultConfigReadable(vault string) error {
    info, err := os.Lstat(filepath.Join(vault, ".obsidian"))
    if err != nil {
        if os.IsNotExist(err) { return errors.New("not an Obsidian vault: missing .obsidian") }
        return fmt.Errorf(".obsidian unreadable: %w", err)
    }
    if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
        return errors.New(".obsidian must be a real directory")
    }
    return nil
}

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: StartObsidianVaultAnalysis(localPath) where the selected directory contains a .obsidian entry that Lstat cannot stat. Concrete cases: .obsidian has restrictive ACL/ownership (read denied to the SiYuan process), resides on an unmounted/unavailable network volume, or hits EIO/ENOTDIR at the filesystem level. It is NOT triggered when .obsidian is simply absent.

Common situations: Vault stored on a network share (SMB/NFS) that dropped mid-session; vault copied from another OS with ownership/permission bits the current user cannot traverse; antivirus/EDR locking the .obsidian folder on Windows; containerised SiYuan where the bind-mount omits the .obsidian subpath or applies read-only mode.

Related errors


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