gastownhall/beads · error

failed to stat config.yaml: %w

Error message

failed to stat config.yaml: %w

What it means

SetYamlConfigInDir stats <beadsDir>/config.yaml before writing; when Stat fails for any reason other than not-exists (permission denied, path is a directory, I/O error), the raw error is wrapped as 'failed to stat config.yaml'. It distinguishes real I/O problems from the friendly not-found message.

Source

Thrown at internal/config/yaml_config.go:296

	return setYamlConfigAtPath(configPath, key, value)
}

// SetYamlConfigInDir sets a configuration value in the config.yaml located in
// the provided beadsDir, bypassing CWD/worktree discovery. Use this when the
// caller has already resolved the authoritative workspace and needs to avoid
// local worktree stubs shadowing the real shared config location.
func SetYamlConfigInDir(beadsDir, key, value string) error {
	// Validate specific keys (GH#995)
	if err := validateYamlConfigValue(key, value); err != nil {
		return err
	}

	configPath := filepath.Join(beadsDir, "config.yaml")
	if _, err := os.Stat(configPath); err != nil {
		if os.IsNotExist(err) {
			return fmt.Errorf("no config.yaml found in %s (run 'bd init' first)", beadsDir)
		}
		return fmt.Errorf("failed to stat config.yaml: %w", err)
	}

	return setYamlConfigAtPath(configPath, key, value)
}

var userGlobalKeyPrefixes = []string{"metrics."}

// userGlobalExactKeys are per-MACHINE settings that must never be written to
// the project .beads/config.yaml, which is a git-TRACKED file (see
// cmd/bd/doctor/gitignore.go: nothing in .beads/.gitignore excludes it). A
// committed value propagates one machine's answer to every clone that pulls
// it, which for these keys is worse than having no value at all.
//
// node_id is the exemplar: it names the beads STORE that grants leases here,
// and the reclaim guard (issueops.ReclaimExpiredLeasesInTx) compares it
// against each lease's granted_node. Commit "node_id: mini" and every replica
// reads "mini", so every comparison matches and the guard is simultaneously
// fully ARMED and fully INERT — laptop reaps mini's leases exactly as if they

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check permissions on the .beads directory and fix ownership: ls -la <dir>/.beads; chown/chmod as needed.
  2. Inspect what config.yaml actually is (file vs directory): if it is a directory, remove or rename it and recreate the file.
  3. Verify the mount is writable and healthy (dmesg / remount rw) if on NFS/network storage.
  4. Read the wrapped %w cause in the message for the exact errno and address it directly.

Example fix

// before (config.yaml is a directory)
rm -rf /proj/.beads/config.yaml
// after
rm -rf /proj/.beads/config.yaml
bd init   # recreates config.yaml as a regular file
Defensive patterns

Strategy: try-catch

Validate before calling

fi, err := os.Lstat(filepath.Join(beadsDir, "config.yaml"))
if err == nil && fi.IsDir() {
    return fmt.Errorf("%s is a directory, expected a file", fi.Name())
}
if err := unix.Access(beadsDir, unix.W_OK); err != nil {
    return fmt.Errorf(".beads dir not writable: %w", err)
}

Try / catch

err := config.SetYamlConfigInDir(dir, key, value)
var pe *fs.PathError
if errors.As(err, &pe) {
    return fmt.Errorf("filesystem problem at %s (%v): check permissions/mount", pe.Path, pe.Err)
}

Prevention

When it happens

Trigger: os.Stat on the config.yaml path fails with a non-ENOENT error: the path exists but is a directory named config.yaml, the .beads directory is unreadable (permissions), or a filesystem/I/O error occurs (EIO, stale NFS mount).

Common situations: config.yaml accidentally replaced by a directory; .beads dir with restrictive ownership after sudo/copy; read-only or disconnected network mounts.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/bd499212693a3d42. Report an issue: GitHub.