gastownhall/beads · error

error merging config file %s: %w

Error message

error merging config file %s: %w

What it means

Initialize wraps errors from viper's MergeInConfig when merging each additional config file after the first. It means one of the overlay config paths exists but could not be parsed or read; the failing path is interpolated into the message. The viper cause is wrapped with %w.

Source

Thrown at internal/config/config.go:368

	// 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")
		if _, err := os.Stat(localConfigPath); err == nil {
			v.SetConfigFile(localConfigPath)
			if err := v.MergeInConfig(); err != nil {
				return fmt.Errorf("error merging local config file: %w", err)
			}
			debug.Logf("Debug: merged local config from %s\n", localConfigPath)
			// Restore primary as ConfigFileUsed

View on GitHub (pinned to 71377f2769)

Solutions

  1. Fix the YAML in the specific file named in the error message (validate with a YAML linter).
  2. Check read permissions on that overlay file for the running user.
  3. Remove or rename the broken overlay file if it is no longer needed; Initialize tolerates missing later paths only if your code filters them beforehand.
  4. Diff the overlay against the primary config to spot schema drift that breaks parsing.

Example fix

// before
bd config add ./prod-overlay.yaml  # file contains: {routing: mode:} 
// after
# prod-overlay.yaml
routing:
  mode: auto
Defensive patterns

Strategy: validation

Validate before calling

for _, p := range overlayPaths {
    data, err := os.ReadFile(p)
    if err != nil { return err }
    var m map[string]interface{}
    if err := yaml.Unmarshal(data, &m); err != nil {
        return fmt.Errorf("overlay %s invalid: %w", p, err)
    }
}

Try / catch

if err := config.Initialize(paths); err != nil {
    if strings.Contains(err.Error(), "error merging config file") {
        // extract path from message or re-run per-file to isolate
        log.Fatalf("bad overlay config: %v", err)
    }
    return err
}

Prevention

When it happens

Trigger: Initialize receives multiple config paths; a path at index >= 1 points to a file with invalid YAML, wrong permissions, or an unreadable directory.

Common situations: Team overlays (e.g. per-environment config files) that diverge in schema; a CI-generated overlay written as JSON with a syntax error; a second config file made read-only by a package manager.

Related errors


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