gastownhall/beads · error
failed to marshal config: %w
Error message
failed to marshal config: %w
What it means
SaveConfigValue reads the existing config into a map, applies the single key change via setNestedKey, then re-serializes with yaml.Marshal before writing. This error wraps a failure of yaml.Marshal, which is rare but possible when the existing config contains values that cannot be marshaled (e.g. unmarshalable types, invalid map keys, cyclic structures injected programmatically).
Source
Thrown at internal/config/config.go:657
configPath := v.ConfigFileUsed()
if configPath == "" {
configPath = filepath.Join(beadsDir, "config.yaml")
v.SetConfigFile(configPath)
}
// Read existing file contents to avoid dumping all merged viper state
// (defaults, env vars, overrides) into the config file.
existing := make(map[string]interface{})
if data, err := os.ReadFile(filepath.Clean(configPath)); err == nil {
_ = yaml.Unmarshal(data, &existing)
}
// Set the single key using dot-path splitting for nested keys (e.g. "routing.mode").
setNestedKey(existing, key, value)
out, err := yaml.Marshal(existing)
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
return os.WriteFile(configPath, out, 0o600)
}
// setNestedKey sets a value in a nested map using a dot-separated key path.
func setNestedKey(m map[string]interface{}, key string, value interface{}) {
parts := strings.SplitN(key, ".", 2)
if len(parts) == 1 {
m[key] = value
return
}
sub, ok := m[parts[0]].(map[string]interface{})
if !ok {
sub = make(map[string]interface{})
m[parts[0]] = sub
}
setNestedKey(sub, parts[1], value)
}View on GitHub (pinned to 71377f2769)
Solutions
- Inspect the config for values set with unusual types and convert them to strings, numbers, bools, slices, or maps before saving.
- Log the wrapped yaml.Marshal error for the offending type; yaml errors name the unmarshalable value.
- Reset the offending key to a plain value with Set before calling SaveConfigValue.
- If caused by a bad SetDefault in code, fix the default to use a marshalable type.
Example fix
// before
v.Set("custom.handler", someFunc) // func is not YAML-marshalable
// after
v.Set("custom.handler", "handlerName") // store a serializable value Defensive patterns
Strategy: try-catch
Validate before calling
// keep values marshalable before saving:
switch v.(type) {
case string, bool, int, float64, []interface{}, map[string]interface{}, nil:
// ok
default:
return fmt.Errorf("value for %s of type %T is not YAML-marshalable", key, value)
} Try / catch
if err := config.SaveConfigValue(key, value, beadsDir); err != nil {
if strings.Contains(err.Error(), "failed to marshal config") {
log.Warn("config contains unmarshalable value; saving as string", "key", key)
return config.SaveConfigValue(key, fmt.Sprintf("%v", value), beadsDir)
}
return err
} Prevention
- Only store strings, numbers, booleans, lists, and maps in config keys.
- Avoid passing funcs, channels, or pointers to exotic types into Set/SetDefault.
- After programmatic Set calls, round-trip the config through Marshal in tests.
- Review third-party integrations that inject config values for serializable types.
When it happens
Trigger: The in-memory config map contains a value of a type gopkg.in/yaml cannot marshal — e.g. a func, channel, or a map with non-string/non-int keys — typically injected earlier via Set/SetDefault or by a corrupted read.
Common situations: A custom default or programmatically set value of an unsupported type ends up in the config tree; a third-party integration stores an exotic value under a config key.
Related errors
- persist sync.remote to config.yaml: %w
- failed to persist sync.remote to config.yaml: %w
- failed to persist sync.remote to config.yaml: %v
- failed to configure hydration: %w
- error reading config file: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/c0548e5a99943ee3.
Report an issue: GitHub.