charmbracelet/crush · error

create config directory

Error message

create config directory: %w

What it means

lockConfig acquires the in-process mutex and a cross-process flock for a config write. Before locking the file it ensures the config directory exists via os.MkdirAll. This error wraps an MkdirAll failure — the OS refused to create the directory that holds the config file.

Solutions

  1. Check permissions on the config path's parent directories and grant write access
  2. Verify no regular file exists where a directory component is expected; remove/rename it
  3. Confirm XDG_CONFIG_HOME/HOME point to a writable location
  4. Check the filesystem is not read-only or out of space (df, mount)

Example fix

// before
os.Setenv("XDG_CONFIG_HOME", "/ro-mount/config") // read-only volume
store.AtomicWrite(...)
// after
os.Setenv("XDG_CONFIG_HOME", filepath.Join(os.TempDir(), "app-config"))
os.MkdirAll(os.Getenv("XDG_CONFIG_HOME"), 0o755) // verify writable before use
Defensive patterns

Strategy: validation

Validate before calling

dir := filepath.Dir(configPath)
if info, err := os.Stat(dir); err != nil {
    if err := os.MkdirAll(dir, 0o755); err != nil {
        return fmt.Errorf("config dir %s not creatable: %w", dir, err)
    }
} else if !info.IsDir() {
    return fmt.Errorf("%s exists but is not a directory", dir)
} else if f, err := os.CreateTemp(dir, ".wtest"); err != nil {
    return fmt.Errorf("config dir %s not writable: %w", dir, err)
} else { f.Close(); os.Remove(f.Name()) }

Type guard

func configDirWritable(dir string) bool {
    fi, err := os.Stat(dir)
    return err == nil && fi.IsDir() && unix.Access(dir, unix.W_OK) == nil
}

Try / catch

var lockErr *fs.PathError
if errors.As(err, &lockErr) && errors.Is(lockErr, fs.ErrPermission) {
    return fmt.Errorf("fix permissions on %s: %w", filepath.Dir(configPath), err)
}

Prevention

When it happens

Trigger: Calling any config-mutating API (atomicWrite -> lockConfig) when the config directory's parent path is missing and cannot be created: permission denied on the parent, a non-directory file exists at a path component, read-only filesystem, or disk full.

Common situations: XDG_CONFIG_HOME or config path pointed at a read-only mount or another user's directory; a regular file occupies ~/.config or the app dir path; running in a sandboxed/containerized env without write access to the home directory.

Related errors


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

Appendix: source

Thrown at internal/config/store.go:280

// lockConfig acquires both the in-process mutex and a cross-process flock
// on the config file for the given scope. Callers that need to do I/O
// between reading and writing (e.g. an HTTP token exchange) must use
// lockConfig explicitly rather than atomicWrite.
//
// The returned release function drops both locks. Callers must call it
// as soon as the file access is complete — no I/O should be performed
// while the lock is held.
func (s *ConfigStore) lockConfig(scope Scope) (func(), error) {
	s.mu.Lock()
	path, err := s.configPath(scope)
	if err != nil {
		s.mu.Unlock()
		return nil, err
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		s.mu.Unlock()
		return nil, fmt.Errorf("create config directory: %w", err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), configLockDeadline)
	defer cancel()
	release, err := lock.File(ctx, path+".lock")
	if err != nil {
		s.mu.Unlock()
		return nil, fmt.Errorf("acquire config lock: %w", err)
	}
	return func() {
		release()
		s.mu.Unlock()
	}, nil
}

// atomicWrite handles the lock-read-transform-write-unlock cycle for
// config file mutations. The fn callback receives the current file
// contents (raw bytes, or {} if the file is missing) and must return the
// new contents. fn must be pure — no I/O, no network calls.

View on GitHub (pinned to 7944b8e522)