charmbracelet/crush · error

failed to read provider cache file: %w

Error message

failed to read provider cache file: %w

What it means

Thrown by cache.Get() when os.ReadFile cannot read the provider cache file at c.path. The cache layer treats a missing/corrupt cache as fatal for that read, so callers (e.g. resolveHyperAPIKey) receive the wrapped OS error. Typically means the file does not exist yet or is unreadable.

Source

Thrown at internal/config/provider.go:274

		}
	}
	// Provider not found in list; prepend it.
	providerList = append([]catwalk.Provider{provider}, providerList...)
}

type cache[T any] struct {
	path string
}

func newCache[T any](path string) cache[T] {
	return cache[T]{path: path}
}

func (c cache[T]) Get() (T, string, error) {
	var v T
	data, err := os.ReadFile(c.path)
	if err != nil {
		return v, "", fmt.Errorf("failed to read provider cache file: %w", err)
	}

	if err := json.Unmarshal(data, &v); err != nil {
		return v, "", fmt.Errorf("failed to unmarshal provider data from cache: %w", err)
	}

	return v, etag.Of(data), nil
}

func (c cache[T]) Store(v T) error {
	slog.Info("Saving provider data to disk", "path", c.path)
	if err := os.MkdirAll(filepath.Dir(c.path), 0o755); err != nil {
		return fmt.Errorf("failed to create directory for provider cache: %w", err)
	}

	data, err := json.Marshal(v)
	if err != nil {
		return fmt.Errorf("failed to marshal provider data: %w", err)

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Ensure the cache file exists by triggering a refresh: run `crush update-providers` (and `crush update-hyper` if applicable).
  2. Check file permissions on the cache path (and parent dirs) so the current user can read it.
  3. If the cache is corrupt/undesired, delete it and let Crush recreate it.
  4. Verify env vars controlling the cache directory (e.g. XDG_CACHE_HOME) point to a valid, writable location.
  5. Wrap the call to tolerate os.IsNotExist and fall back to fetching the catalog.

Example fix

// before
v, _, err := cache.Get()
// after
v, _, err := cache.Get()
if errors.Is(err, fs.ErrNotExist) {
    err = cache.Store(fresh) // or fetch from network
}
Defensive patterns

Strategy: fallback

Validate before calling

if _, err := os.Stat(cachePath); errors.Is(err, fs.ErrNotExist) {
    // skip cache read; fetch fresh data
}

Type guard

func cacheReadable(path string) bool {
    f, err := os.Open(path)
    if err != nil { return false }
    f.Close()
    return true
}

Try / catch

v, etag, err := c.Get()
if err != nil {
    if errors.Is(err, fs.ErrNotExist) { v = fetchFresh() } else { return err }
}

Prevention

When it happens

Trigger: Calling cache.Get() (directly or via resolveHyperAPIKey / the syncer's anonymous getter) before the cache file was ever written; the file was deleted; wrong permissions; c.path points to a nonexistent directory.

Common situations: First run before any cache store; user or cleanup tool removed ~/.cache/crush files; permission restrictions after switching users; cache path misconfigured via env (XDG dirs).

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


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