charmbracelet/crush · error

failed to read file: %w

Error message

failed to read file: %w

What it means

When UpdateProviders' pathOrURL is neither "embedded" nor an http(s) URL, it is treated as a filesystem path and read with os.ReadFile. This error wraps the read failure (file not found, permission denied, is-a-directory). It indicates the local provider JSON source could not be opened.

Source

Thrown at internal/config/provider.go:74

// UpdateProviders updates the Catwalk providers list from a specified source.
func UpdateProviders(pathOrURL string) error {
	var providers []catwalk.Provider
	pathOrURL = cmp.Or(pathOrURL, os.Getenv("CATWALK_URL"), defaultCatwalkURL)

	switch {
	case pathOrURL == "embedded":
		providers = embedded.GetAll()
	case strings.HasPrefix(pathOrURL, "http://") || strings.HasPrefix(pathOrURL, "https://"):
		var err error
		providers, err = catwalk.NewWithURL(pathOrURL).GetProviders(context.Background(), "")
		if err != nil {
			return fmt.Errorf("failed to fetch providers from Catwalk: %w", err)
		}
	default:
		content, err := os.ReadFile(pathOrURL)
		if err != nil {
			return fmt.Errorf("failed to read file: %w", err)
		}
		if err := json.Unmarshal(content, &providers); err != nil {
			return fmt.Errorf("failed to unmarshal provider data: %w", err)
		}
		if len(providers) == 0 {
			return fmt.Errorf("no providers found in the provided source")
		}
	}

	if err := newCache[[]catwalk.Provider](cachePathFor("providers")).Store(providers); err != nil {
		return fmt.Errorf("failed to save providers to cache: %w", err)
	}

	slog.Info("Providers updated successfully", "count", len(providers), "from", pathOrURL, "to", cachePathFor)
	return nil
}

// resolveHyperAPIKey returns the Hyper API key from the environment or

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Verify the path exists: `ls -l <path>` and fix typos or pass an absolute path.
  2. Expand `~` yourself before calling (os.UserHomeDir or filepath.Join).
  3. Fix permissions with chmod/chown so the process user can read the file.
  4. If it should be remote, prefix the URL with http:// or https:// so it is not treated as a file path.

Example fix

// before
err := config.UpdateProviders("~/providers.json")
// after
home, _ := os.UserHomeDir()
err := config.UpdateProviders(filepath.Join(home, "providers.json"))
Defensive patterns

Strategy: validation

Validate before calling

path := src
if strings.HasPrefix(path, "~") {
    home, _ := os.UserHomeDir()
    path = filepath.Join(home, path[1:])
}
if fi, err := os.Stat(path); err != nil {
    return fmt.Errorf("provider source not accessible: %w", err)
} else if fi.IsDir() {
    return fmt.Errorf("provider source is a directory: %s", path)
}

Try / catch

if _, err := os.Stat(path); err != nil {
    if errors.Is(err, os.ErrNotExist) {
        slog.Error("provider file missing", "path", path)
    } else if errors.Is(err, os.ErrPermission) {
        slog.Error("provider file unreadable", "path", path)
    }
    return err
}

Prevention

When it happens

Trigger: Calling config.UpdateProviders with a local path (e.g. "providers.json", "~/catwalk.json") that does not exist, has wrong permissions, is a directory, or contains an unexpanded `~` which Go does not expand.

Common situations: Typo in the path; running from a different working directory with a relative path; forgetting that `~` is not shell-expanded before reaching os.ReadFile; file created by another user with 0600 perms.

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/69935961f067b734. Report an issue: GitHub.