charmbracelet/crush · error

create config directory: %w

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.

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)

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.