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
- 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.
- 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.
- On Windows, disable or whitelist the vault in antivirus/EDR that may hold an exclusive handle on .obsidian.
- 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
- Run the SiYuan process as a user with read/traverse rights on the whole vault subtree.
- Avoid pointing the importer at network/removable volumes; copy to a local dir first.
- Whitelist the vault path in antivirus/EDR on Windows.
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
- Obsidian Vault is unreadable: read Vault root: %v
- read Vault directory [%s]: %w
- import path is not sub path of import dir
- not a valid workspace: %s
- invalid .sy base name [%s]: must end with .sy
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/74e80df75aac94c0.
Report an issue: GitHub.