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

  1. Inspect the wrapped error for the offending field/type name
  2. Remove or JSON-encode any non-serializable fields added to Config by custom code
  3. Fix any custom MarshalJSON implementation to return encodable values
  4. 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

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


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