charmbracelet/crush · error

failed to set config field %s: %w

Error message

failed to set config field %s: %w

What it means

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.

Source

Thrown at internal/config/store.go:406

// already published an updated clone and capture the snapshot themselves
// (update). Both of those run under writeMu, which is what keeps the
// snapshot map free of concurrent writers.
func (s *ConfigStore) writeConfigFields(scope Scope, kv map[string]any) error {
	// Sort keys for deterministic output regardless of map iteration
	// order. This also ensures consistent results when callers pass
	// overlapping JSONPath keys (e.g. "a" and "a.b").
	keys := make([]string, 0, len(kv))
	for k := range kv {
		keys = append(keys, k)
	}
	slices.Sort(keys)

	return s.atomicWrite(scope, func(data []byte) ([]byte, error) {
		v := string(data)
		for _, key := range keys {
			var sErr error
			if v, sErr = sjson.Set(v, key, kv[key]); sErr != nil {
				return nil, fmt.Errorf("failed to set config field %s: %w", key, sErr)
			}
		}
		return []byte(v), nil
	})
}

// mutateInMemory applies a copy-on-write change to the config without
// persisting. Under writeMu it clones the live config, lets mutate edit the
// clone, and publishes it. This is the single primitive every in-memory
// config change goes through, so a published Config is never mutated in
// place and readers always see a consistent snapshot.
func (s *ConfigStore) mutateInMemory(mutate func(*Config)) {
	s.writeMu.Lock()
	defer s.writeMu.Unlock()

	nc := s.Config().cloneForWrite()
	mutate(nc)
	s.setConfig(nc)

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Validate the config file parses as JSON (jq . crush.json) and fix syntax errors
  2. Check that intermediate path segments hold objects, not scalars
  3. Escape or sanitize dots/special chars in dynamic keys (provider IDs) when building the sjson path
  4. Upgrade to a version with the fix if the path syntax is the culprit

Example fix

// before
s.SetConfigField(scope, "providers.my.org/api.api_key", k) // dots parsed as path segments
// after
path := fmt.Sprintf("providers.%s.api_key", sjsonEscapeKey(providerID)) // e.g. "my\\.org/api"
s.SetConfigField(scope, path, k)
Defensive patterns

Strategy: validation

Validate before calling

raw, err := os.ReadFile(configPath)
if err == nil && len(raw) > 0 {
    if !json.Valid(raw) {
        return fmt.Errorf("config file contains invalid JSON; fix before writing")
    }
}
// also verify the path segment parent is an object
var m map[string]any
json.Unmarshal(raw, &m); _, ok := m["providers"]; if !ok { return errors.New("no providers object") }

Type guard

func canSetPath(data []byte, path string) bool {
    return gjson.GetBytes(data, strings.Split(path, ".")[0]).Exists() || len(data) == 0
}

Try / catch

if err := store.SetConfigField(scope, key, val); err != nil {
    var handled bool
    if strings.Contains(err.Error(), "failed to set config field") {
        fmt.Fprintf(os.Stderr, "check JSON validity of config and path syntax for %s: %v\n", key, err)
        handled = true
    }
    if !handled { return err }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/157dcee1dc2c35ed. Report an issue: GitHub.