gastownhall/beads · error

error reading config file: %w

Error message

error reading config file: %w

What it means

Initialize wraps any error returned by viper's ReadInConfig when loading the first (primary) config file. It means the primary config file exists but could not be read or parsed — bad YAML syntax, unreadable permissions, or a directory passed as a config path. The underlying viper error is preserved via %w so callers can inspect the cause.

Source

Thrown at internal/config/config.go:361

	v.SetDefault("ai.base_url", "")

	// List command defaults
	v.SetDefault("list.limit", 50)

	// Output configuration (GH#1384)
	// Controls title display in command feedback messages.
	// 0 = hide title, N > 0 = truncate to N chars with "…"
	v.SetDefault("output.title-length", 255)

	// External projects for cross-project dependency resolution (bd-h807)
	// Maps project names to paths for resolving external: blocked_by references
	v.SetDefault("external_projects", map[string]string{})

	// Load config files: lowest priority first, each MergeInConfig overwrites
	if len(configPaths) > 0 {
		v.SetConfigFile(configPaths[0])
		if err := v.ReadInConfig(); err != nil {
			return fmt.Errorf("error reading config file: %w", err)
		}
		debug.Logf("Debug: loaded config from %s\n", configPaths[0])

		for _, p := range configPaths[1:] {
			v.SetConfigFile(p)
			if err := v.MergeInConfig(); err != nil {
				return fmt.Errorf("error merging config file %s: %w", p, err)
			}
			debug.Logf("Debug: merged config from %s\n", p)
		}

		// Restore primary config path as ConfigFileUsed (used by SaveConfigValue,
		// ResolveExternalProjectPath, etc.)
		v.SetConfigFile(primaryConfigPath)

		// Merge local config overrides if present (config.local.yaml)
		// This allows machine-specific settings without polluting tracked config
		localConfigPath := filepath.Join(filepath.Dir(primaryConfigPath), "config.local.yaml")

View on GitHub (pinned to 71377f2769)

Solutions

  1. Open the file named in the wrapped error and fix the YAML syntax (validate with yamllint or a YAML parser).
  2. Check file permissions and ownership: ensure the current user can read the file.
  3. Verify the path points to a regular file, not a directory or symlink to a missing target.
  4. If the file is unrecoverable, restore it from version control or a backup, or delete it so defaults are used.

Example fix

// before (config.yaml)
routing:
	mode: auto  # tab indentation breaks YAML
// after
routing:
  mode: auto  # use spaces
Defensive patterns

Strategy: validation

Validate before calling

info, err := os.Stat(configPath)
if err != nil { return err }
if info.IsDir() { return fmt.Errorf("%s is a directory, not a file", configPath) }
data, err := os.ReadFile(configPath)
if err != nil { return err }
if err := yaml.Unmarshal(data, map[string]interface{}{}); err != nil {
    return fmt.Errorf("config %s has invalid YAML: %w", configPath, err)
}

Try / catch

if err := config.Initialize(paths); err != nil {
    var perr *fs.PathError
    if errors.As(err, &perr) {
        log.Fatalf("cannot read %s: %v — fix or remove the file", perr.Path, perr.Err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling Initialize with a configPaths[0] file that is malformed YAML, has no read permission, is a directory, or an empty/corrupt file that viper's ReadInConfig rejects.

Common situations: Hand-edited config.yaml with broken indentation or tabs; a config file truncated by a crashed editor or disk-full write; permissions changed by another user; passing a path with the wrong extension or encoding (UTF-16, BOM issues).

Related errors


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