siyuan-note/siyuan · error

read Vault directory [%s]: %w

Error message

read Vault directory [%s]: %w

What it means

Thrown inside scanObsidianVaultFiles when os.ReadDir(absDir) fails while walking the vault tree. Unlike the root-config error (780), this is a raw fmt.Errorf (not wrapped in an obsidianUserError sentinel), so the task detail layer falls through to the generic code 351 unless an outer handler re-wraps it. It aborts the recursive scan that builds the file/asset inventory used by every later analysis stage.

Source

Thrown at kernel/model/import_obsidian.go:603

	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 {
			return err
		}
		name := entry.Name()
		rel := path.Join(relDir, filepath.ToSlash(name))
		abs := filepath.Join(absDir, name)
		if strings.HasPrefix(name, ".") {
			if relDir == "" && name == ".obsidian" {
				continue
			}
			vault.Analysis.SkippedHiddenCount++
			continue
		}
		entryInfo, infoErr := entry.Info()
		if infoErr != nil {

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Identify the failing subdirectory from the wrapped error path ([relDir]) and fix its read/execute bits: chmod -R u+rX <vault>.
  2. If only some subdirs are inaccessible and unneeded, exclude them from the vault or move them out before analysis.
  3. For cloud 'online-only' folders, force a local download of the whole vault (or disable online-only for that folder) and retry.
  4. Re-run StartObsidianVaultAnalysis after the filesystem is healthy; the task system auto-replaces a queued/analysing task.

Example fix

// before: nested dir not readable by SiYuan user
//   /vault/notes/private  (mode 700, owned by other user)

// after: make traversable
chmod -R u+rX /vault && chown -R $(whoami) /vault
Defensive patterns

Strategy: validation

Validate before calling

// Walk the vault and confirm every subdir is readable before analysis.
func vaultFullyReadable(root string) error {
    return filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
        if err != nil { return err }
        if d.IsDir() {
            if _, e := os.ReadDir(p); e != nil { return fmt.Errorf("unreadable dir %s: %w", p, e) }
        }
        return nil
    })
}

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: Any subdirectory of the vault that ReadDir cannot enumerate: permission-denied on a subdir (EACCES), a path that was deleted between root validation and descent (ENOENT), a removed removable medium, or a directory whose mode bits forbid the running user. Because the scan walks every entry, a single unreadable nested folder fails the whole task.

Common situations: Vault contains a folder owned by another user with mode 700; vault on a USB/external drive unplugged mid-analysis; cloud-sync stubs (Dropbox/OneDrive 'online-only' placeholders) that error on ReadDir when offline; SELinux/AppArmor denying readdir to the SiYuan process.

Related errors


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