{"record":{"id":"157dcee1dc2c35ed","repo":"charmbracelet/crush","slug":"failed-to-set-config-field-s-w","errorCode":null,"errorMessage":"failed to set config field %s: %w","messagePattern":"failed to set config field (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/config/store.go","lineNumber":406,"sourceCode":"// already published an updated clone and capture the snapshot themselves\n// (update). Both of those run under writeMu, which is what keeps the\n// snapshot map free of concurrent writers.\nfunc (s *ConfigStore) writeConfigFields(scope Scope, kv map[string]any) error {\n\t// Sort keys for deterministic output regardless of map iteration\n\t// order. This also ensures consistent results when callers pass\n\t// overlapping JSONPath keys (e.g. \"a\" and \"a.b\").\n\tkeys := make([]string, 0, len(kv))\n\tfor k := range kv {\n\t\tkeys = append(keys, k)\n\t}\n\tslices.Sort(keys)\n\n\treturn s.atomicWrite(scope, func(data []byte) ([]byte, error) {\n\t\tv := string(data)\n\t\tfor _, key := range keys {\n\t\t\tvar sErr error\n\t\t\tif v, sErr = sjson.Set(v, key, kv[key]); sErr != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to set config field %s: %w\", key, sErr)\n\t\t\t}\n\t\t}\n\t\treturn []byte(v), nil\n\t})\n}\n\n// mutateInMemory applies a copy-on-write change to the config without\n// persisting. Under writeMu it clones the live config, lets mutate edit the\n// clone, and publishes it. This is the single primitive every in-memory\n// config change goes through, so a published Config is never mutated in\n// place and readers always see a consistent snapshot.\nfunc (s *ConfigStore) mutateInMemory(mutate func(*Config)) {\n\ts.writeMu.Lock()\n\tdefer s.writeMu.Unlock()\n\n\tnc := s.Config().cloneForWrite()\n\tmutate(nc)\n\ts.setConfig(nc)","sourceCodeStart":388,"sourceCodeEnd":424,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/config/store.go#L388-L424","documentation":"writeConfigFields applies sjson.Set for each key inside the atomicWrite transform. This error wraps an sjson parse/mutation failure — sjson could not parse the current JSON or could not set the given path, so the write is aborted.","triggerScenarios":"Calling SetConfigField with a key/path that sjson cannot apply (e.g. setting a child under a scalar value like providers.foo.api_key.nested, or setting an array index on a non-array), or the existing config file contains invalid JSON.","commonSituations":"Hand-edited or corrupted crush.json with malformed JSON; a path collision where an intermediate key holds a string/number instead of an object; programmatic key built with a typo or special characters (dots in provider IDs) that sjson misinterprets as path segments.","solutions":["Validate the config file parses as JSON (jq . crush.json) and fix syntax errors","Check that intermediate path segments hold objects, not scalars","Escape or sanitize dots/special chars in dynamic keys (provider IDs) when building the sjson path","Upgrade to a version with the fix if the path syntax is the culprit"],"exampleFix":"// before\ns.SetConfigField(scope, \"providers.my.org/api.api_key\", k) // dots parsed as path segments\n// after\npath := fmt.Sprintf(\"providers.%s.api_key\", sjsonEscapeKey(providerID)) // e.g. \"my\\\\.org/api\"\ns.SetConfigField(scope, path, k)","handlingStrategy":"validation","validationCode":"raw, err := os.ReadFile(configPath)\nif err == nil && len(raw) > 0 {\n    if !json.Valid(raw) {\n        return fmt.Errorf(\"config file contains invalid JSON; fix before writing\")\n    }\n}\n// also verify the path segment parent is an object\nvar m map[string]any\njson.Unmarshal(raw, &m); _, ok := m[\"providers\"]; if !ok { return errors.New(\"no providers object\") }","typeGuard":"func canSetPath(data []byte, path string) bool {\n    return gjson.GetBytes(data, strings.Split(path, \".\")[0]).Exists() || len(data) == 0\n}","tryCatchPattern":"if err := store.SetConfigField(scope, key, val); err != nil {\n    var handled bool\n    if strings.Contains(err.Error(), \"failed to set config field\") {\n        fmt.Fprintf(os.Stderr, \"check JSON validity of config and path syntax for %s: %v\\n\", key, err)\n        handled = true\n    }\n    if !handled { return err }\n}","preventionTips":["Never hand-edit config files without validating with jq afterward","Escape dots in dynamic keys (provider IDs) when building sjson paths","Ensure intermediate JSON nodes are objects before setting nested paths","Use the same key-naming scheme for set and delete operations"],"tags":["json","sjson","config-write"],"backgroundTag":"json-path-set-failed","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}