siyuan-note/siyuan · error · errObsidianVaultUnreadable

%w: read Vault config directory: %v

Error message

%w: read Vault config directory: %v

What it means

After locating the vault root, the importer must read its `.obsidian` configuration directory to confirm this is a real Obsidian vault. When `os.Lstat("<vault>/.obsidian")` fails with an error other than not-exist (e.g. permission denied, I/O error, or a filesystem-level failure), the import aborts with this wrapped error. A not-exist error is reported separately as errObsidianVaultConfigMissing.

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 8641553a1f)

Solutions

  1. Check that `<vault>/.obsidian` exists and is readable: run `ls -la <vault>/.obsidian`
  2. Fix directory/file permissions so the user running SiYuan can stat and read the path
  3. Remount the drive the vault lives on if it is external or networked
  4. Exclude the vault from antivirus/quarantine tools that lock metadata access

Example fix

// before: unreadable permissions
chmod 000 ~/.config/obsidian/MyVault/.obsidian

// after
chmod 755 ~/.config/obsidian/MyVault/.obsidian
Defensive patterns

Strategy: validation

Validate before calling

import fs from "fs"
try {
  const info = fs.lstatSync(path.join(vaultRoot, ".obsidian"))
  if (!info.isDirectory()) throw new Error(".obsidian is not a directory")
} catch (e) {
  throw new Error(`Cannot read vault config directory: ${e.message}`)
}

Try / catch

try {
  fs.accessSync(path.join(vaultRoot, ".obsidian"), fs.constants.R_OK)
} catch (e) {
  logError("Vault config directory unreadable", e)
  // surface a user-facing message and abort before import
}

Prevention

When it happens

Trigger: os.Lstat on `<vaultRoot>/.obsidian` returns any error except os.IsNotExist: read permission denied on the vault directory, I/O error on the filesystem, name too long, or the Lstat syscall failing for other OS reasons.

Common situations: Vault on an external/network drive that disconnected; directory permissions changed by sync or antivirus tooling; vault mounted read-only with restrictive ACLs; a broken fuse mount.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/5f439dbda5f7bfa6. Report an issue: GitHub.