Version / branch / commit
Audited on main at 903e3d13 (2026-08-23).
Problem
Most config mutators load JSON into the typed config struct and writeConfigFile rewrites the complete document with json.MarshalIndent. Fields unknown to the current binary are not represented by the struct and disappear on the next write.
Reproduction
Start with a valid config containing an additional field:
{
"theme": "default",
"futureSetting": {"a": 1}
}
Run a normal mutation such as SetTheme. The resulting file no longer contains futureSetting.
Expected behavior
Config writes preserve unknown fields so a current or older Zero binary does not destroy settings written by a newer version or extension.
Actual behavior
Any mutator that routes through writeConfigFile silently removes every unmodelled top-level field.
Relevant code
internal/config/writer.go:767-773
internal/config/unknownfields.go:14-22
internal/config/writer.go:596-621 (SetPet already uses a surgical raw-JSON update to avoid this class)
Suggested fix
Preserve extra top-level members as map[string]json.RawMessage through custom marshal/unmarshal logic, or generalize the raw-JSON mutation approach. Add a regression test proving an unknown object survives a representative config mutation.
Version / branch / commit
Audited on
mainat903e3d13(2026-08-23).Problem
Most config mutators load JSON into the typed config struct and
writeConfigFilerewrites the complete document withjson.MarshalIndent. Fields unknown to the current binary are not represented by the struct and disappear on the next write.Reproduction
Start with a valid config containing an additional field:
{ "theme": "default", "futureSetting": {"a": 1} }Run a normal mutation such as
SetTheme. The resulting file no longer containsfutureSetting.Expected behavior
Config writes preserve unknown fields so a current or older Zero binary does not destroy settings written by a newer version or extension.
Actual behavior
Any mutator that routes through
writeConfigFilesilently removes every unmodelled top-level field.Relevant code
internal/config/writer.go:767-773internal/config/unknownfields.go:14-22internal/config/writer.go:596-621(SetPetalready uses a surgical raw-JSON update to avoid this class)Suggested fix
Preserve extra top-level members as
map[string]json.RawMessagethrough custom marshal/unmarshal logic, or generalize the raw-JSON mutation approach. Add a regression test proving an unknown object survives a representative config mutation.