{"record":{"id":"74e80df75aac94c0","repo":"siyuan-note/siyuan","slug":"obsidian-vault-is-unreadable-read-vault-config-di","errorCode":null,"errorMessage":"Obsidian Vault is unreadable: read Vault config directory: %v","messagePattern":"Obsidian Vault is unreadable: read Vault config directory: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/import_obsidian.go","lineNumber":589,"sourceCode":"\t\treturn \"\", errObsidianVaultNotDirectory\n\t}\n\tif info.Mode()&os.ModeSymlink != 0 || isObsidianResolvedLink(abs) {\n\t\treturn \"\", fmt.Errorf(\"%w: Vault root is a symbolic link or reparse point\", errObsidianVaultUnsafePath)\n\t}\n\tif util.IsSensitivePath(abs) {\n\t\treturn \"\", fmt.Errorf(\"%w: selected Vault path is sensitive\", errObsidianVaultUnsafePath)\n\t}\n\tworkspace, _ := filepath.Abs(filepath.Clean(util.WorkspaceDir))\n\tif sameObsidianPath(abs, workspace) || gulu.File.IsSubPath(workspace, abs) || gulu.File.IsSubPath(abs, workspace) {\n\t\treturn \"\", fmt.Errorf(\"%w: Vault root and SiYuan workspace contain each other\", errObsidianVaultUnsafePath)\n\t}\n\tconfigPath := filepath.Join(abs, \".obsidian\")\n\tconfigInfo, statErr := os.Lstat(configPath)\n\tif statErr != nil {\n\t\tif os.IsNotExist(statErr) {\n\t\t\treturn \"\", errObsidianVaultConfigMissing\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"%w: read Vault config directory: %v\", errObsidianVaultUnreadable, statErr)\n\t}\n\tif !configInfo.IsDir() || configInfo.Mode()&os.ModeSymlink != 0 || isObsidianResolvedLink(configPath) {\n\t\treturn \"\", errObsidianVaultConfigMissing\n\t}\n\treturn abs, nil\n}\n\nfunc scanObsidianVaultFiles(ctx context.Context, vault *obsidianVaultContext, relDir, absDir string) error {\n\tif err := ctx.Err(); err != nil {\n\t\treturn err\n\t}\n\tentries, err := os.ReadDir(absDir)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"read Vault directory [%s]: %w\", relDir, err)\n\t}\n\tsort.Slice(entries, func(i, j int) bool { return strings.ToLower(entries[i].Name()) < strings.ToLower(entries[j].Name()) })\n\tfor _, entry := range entries {\n\t\tif err = ctx.Err(); err != nil {","sourceCodeStart":571,"sourceCodeEnd":607,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/import_obsidian.go#L571-L607","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: vault on a read-only / locked network share\nStartObsidianVaultAnalysis(\"/mnt/nfs/vault\")  // Lstat(.obsidian) -> EACCES\n\n// after: copy to a local, fully-owned path\ncp -r /mnt/nfs/vault ~/vault && chmod -R u+rwX ~/vault\nStartObsidianVaultAnalysis(filepath.Join(home, \"vault\"))","handlingStrategy":"validation","validationCode":"// Pre-flight: ensure <vault>/.obsidian is stat-able by this process before analysis.\nfunc vaultConfigReadable(vault string) error {\n    info, err := os.Lstat(filepath.Join(vault, \".obsidian\"))\n    if err != nil {\n        if os.IsNotExist(err) { return errors.New(\"not an Obsidian vault: missing .obsidian\") }\n        return fmt.Errorf(\".obsidian unreadable: %w\", err)\n    }\n    if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {\n        return errors.New(\".obsidian must be a real directory\")\n    }\n    return nil\n}","typeGuard":"null","tryCatchPattern":"null","preventionTips":["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."],"tags":["obsidian-import","filesystem","permissions","validation"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}