gastownhall/beads · error
marshaling config: %w
Error message
marshaling config: %w
What it means
Config.Save() serializes the Config with json.MarshalIndent before writing metadata.json atomically. This error wraps a marshal failure, which is nearly impossible for this struct (all fields are JSON-safe), so it indicates a programming-level inconsistency rather than user environment problems. The save aborts without touching the file on disk.
Source
Thrown at internal/configfile/configfile.go:164
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parsing config: %w", err)
}
return &cfg, nil
}
func (c *Config) Save(beadsDir string) error {
configPath := ConfigPath(beadsDir)
saved := *c
if filepath.IsAbs(saved.DoltDataDir) {
saved.DoltDataDir = ""
}
data, err := json.MarshalIndent(&saved, "", " ")
if err != nil {
return fmt.Errorf("marshaling config: %w", err)
}
// Write-temp-then-rename: a plain os.WriteFile truncates in place, so a
// concurrent Load can observe an empty or partial metadata.json and feed
// store selection a corrupt config. Rename within the same directory is
// atomic, so readers see either the old or the new file, never a torn one.
if err := writeFileAtomic(configPath, data, 0o600); err != nil {
return fmt.Errorf("writing config: %w", err)
}
return nil
}
// writeFileAtomic writes data to a temp file in path's directory and renames
// it over path, so concurrent readers never observe a truncated or partial
// file.
func writeFileAtomic(path string, data []byte, perm os.FileMode) error {
dir := filepath.Dir(path)View on GitHub (pinned to 71377f2769)
Solutions
- Inspect the wrapped error for the offending field/type name
- Remove or JSON-encode any non-serializable fields added to Config by custom code
- Fix any custom MarshalJSON implementation to return encodable values
- If using stock bd, this should not occur — file a bug with the full wrapped error
Example fix
// before
type Config struct { Hook func() `json:"hook"` } // func is not marshalable
// after
type Config struct { HookName string `json:"hook_name,omitempty"` } // marshalable data only Defensive patterns
Strategy: try-catch
Try / catch
if err := cfg.Save(beadsDir); err != nil {
var marshalErr *json.MarshalerError
if errors.As(err, &marshalErr) {
return fmt.Errorf("Config contains non-serializable field: %w", marshalErr)
}
return err
} Prevention
- Only add JSON-encodable fields (string/int/bool/struct) to Config
- Round-trip test: Marshal then Unmarshal in unit tests after struct changes
- Avoid custom MarshalJSON on Config fields unless returning encodable types
- If embedding/extending bd, keep extension state out of the persisted Config struct
When it happens
Trigger: Calling Config.Save(beadsDir) (directly or via Load's migration, finalizeSyncedBootstrap, or context-info paths) only if the Config struct or a registered extension field contains a value json.MarshalIndent cannot encode, e.g. a channel/func added by an embedder, or a custom MarshalJSON returning an error or unsupported type.
Common situations: Custom builds or extensions that added non-serializable fields to Config; a custom json.Marshaler implementation returning an error; runtime data (like an invalid number NaN via a custom marshaler) injected into the struct.
Related errors
- failed to marshal backup state: %w
- failed to marshal issue %s: %w
- failed to write JSON: %w
- failed to load config: %w
- failed to load config: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/997e5c3c3a719ba6.
Report an issue: GitHub.