{"record":{"id":"997e5c3c3a719ba6","repo":"gastownhall/beads","slug":"marshaling-config-w","errorCode":null,"errorMessage":"marshaling config: %w","messagePattern":"marshaling config: %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/configfile/configfile.go","lineNumber":164,"sourceCode":"\n\tvar cfg Config\n\tif err := json.Unmarshal(data, &cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"parsing config: %w\", err)\n\t}\n\treturn &cfg, nil\n}\n\nfunc (c *Config) Save(beadsDir string) error {\n\tconfigPath := ConfigPath(beadsDir)\n\n\tsaved := *c\n\tif filepath.IsAbs(saved.DoltDataDir) {\n\t\tsaved.DoltDataDir = \"\"\n\t}\n\n\tdata, err := json.MarshalIndent(&saved, \"\", \"  \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"marshaling config: %w\", err)\n\t}\n\n\t// Write-temp-then-rename: a plain os.WriteFile truncates in place, so a\n\t// concurrent Load can observe an empty or partial metadata.json and feed\n\t// store selection a corrupt config. Rename within the same directory is\n\t// atomic, so readers see either the old or the new file, never a torn one.\n\tif err := writeFileAtomic(configPath, data, 0o600); err != nil {\n\t\treturn fmt.Errorf(\"writing config: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// writeFileAtomic writes data to a temp file in path's directory and renames\n// it over path, so concurrent readers never observe a truncated or partial\n// file.\nfunc writeFileAtomic(path string, data []byte, perm os.FileMode) error {\n\tdir := filepath.Dir(path)","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/configfile/configfile.go#L146-L182","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\ntype Config struct { Hook func() `json:\"hook\"` } // func is not marshalable\n// after\ntype Config struct { HookName string `json:\"hook_name,omitempty\"` } // marshalable data only","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := cfg.Save(beadsDir); err != nil {\n\tvar marshalErr *json.MarshalerError\n\tif errors.As(err, &marshalErr) {\n\t\treturn fmt.Errorf(\"Config contains non-serializable field: %w\", marshalErr)\n\t}\n\treturn err\n}","preventionTips":["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"],"tags":["json","config","serialization"],"backgroundTag":"config-serialization-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}