gastownhall/beads · error

error merging local config file: %w

Error message

error merging local config file: %w

What it means

Initialize merges an optional machine-local override file, config.local.yaml, sitting next to the primary config, and wraps any MergeInConfig failure from it. It means the local override exists but could not be read or parsed. The viper cause is preserved via %w.

Source

Thrown at internal/config/config.go:383

		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
			v.SetConfigFile(primaryConfigPath)
		}
	} else {
		// No config.yaml found - use defaults and environment variables
		debug.Logf("Debug: no config.yaml found; using defaults and environment variables\n")
	}

	return nil
}

// ResetForTesting clears the config state, allowing Initialize() to be called again.
// This is intended for tests that need to change config.yaml between test steps.
// WARNING: Not thread-safe. Only call from single-threaded test contexts.
func ResetForTesting() {
	v = nil

View on GitHub (pinned to 71377f2769)

Solutions

  1. Open config.local.yaml (path shown via debug log, next to your primary config) and fix the YAML syntax.
  2. Check file permissions on config.local.yaml.
  3. If the overrides are not needed, delete or rename config.local.yaml so it is skipped by the os.Stat check.
  4. Validate with a YAML parser before saving, e.g. `yamllint config.local.yaml`.

Example fix

// before (config.local.yaml)
ai:
  api_key: "sk-... # missing closing quote
// after
ai:
  api_key: "sk-..."
Defensive patterns

Strategy: validation

Validate before calling

local := filepath.Join(filepath.Dir(primaryConfigPath), "config.local.yaml")
if _, err := os.Stat(local); err == nil {
    var m map[string]interface{}
    data, _ := os.ReadFile(local)
    if err := yaml.Unmarshal(data, &m); err != nil {
        return fmt.Errorf("fix %s before starting: %w", local, err)
    }
}

Try / catch

if err := config.Initialize(paths); err != nil {
    if strings.Contains(err.Error(), "config.local") {
        log.Warn("local override broken, ignoring", "err", err)
        os.Rename(local, local+".broken")
        err = config.Initialize(paths)
    }
    return err
}

Prevention

When it happens

Trigger: A config.local.yaml exists in the same directory as the primary config, and its YAML is invalid or the file is unreadable when Initialize runs.

Common situations: A developer hand-edits config.local.yaml for machine-specific settings (API keys, paths) and introduces a YAML typo; the file is copied between machines with different encodings; a lockfile-style collision leaves it half-written.

Related errors


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