charmbracelet/crush · warning

cannot stat %s: %w

Error message

cannot stat %s: %w

What it means

probeEnt starts by stat-ing the candidate path to confirm it exists; any error is wrapped as "cannot stat %s". Callers (Lookup/LookupClosest variants) then filter os.ErrNotExist and os.ErrPermission, so this message normally surfaces only for unexpected stat failures, but it is also the internal carrier for ENOENT/EACCES.

Source

Thrown at internal/fsext/lookup.go:263

		return err
	}
}

// canonicalize resolves any symbolic links in path. If resolution fails
// (typically because path does not exist yet) the original path is
// returned cleaned, so callers can still perform stable equality checks.
func canonicalize(path string) string {
	if resolved, err := filepath.EvalSymlinks(path); err == nil {
		return resolved
	}
	return filepath.Clean(path)
}

// probeEnt checks if entity at given path exists and belongs to given owner
func probeEnt(fspath string, owner int) error {
	_, err := os.Stat(fspath)
	if err != nil {
		return fmt.Errorf("cannot stat %s: %w", fspath, err)
	}

	// special case for ownership check bypass
	if owner == -1 {
		return nil
	}

	fowner, err := Owner(fspath)
	if err != nil {
		return fmt.Errorf("cannot get ownership for %s: %w", fspath, err)
	}

	if fowner != owner {
		return os.ErrPermission
	}

	return nil
}

View on GitHub (pinned to 7944b8e522)

Solutions

  1. If the wrapped error is os.ErrNotExist or os.ErrPermission, it is expected and already skipped — no action needed
  2. For ENOTDIR, ensure no target-name component is a regular file in ancestors
  3. Break symlink loops (ELOOP) and check mount health for EIO
  4. Unwrap with errors.Is to branch on the specific errno in your own handling code

Example fix

// before
_, err := fsext.Lookup(dir, target)
if err != nil {
    panic(err)
}
// after
_, err := fsext.Lookup(dir, target)
if err != nil {
    if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) {
        return nil // expected during upward lookup
    }
    return err
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Check candidate exists before treating failure as unexpected
p := filepath.Join(dir, target)
if _, err := os.Stat(p); err != nil {
    if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) {
        return nil // expected: lookup skips these
    }
}

Type guard

func probeErrIsBenign(err error) bool {
    return errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission)
}

Try / catch

_, err := fsext.Lookup(dir, targets...)
if err != nil {
    var pe *os.PathError
    if errors.As(err, &pe) {
        switch {
        case errors.Is(pe.Err, syscall.ENOTDIR):
            return handleComponentNotDir(pe.Path)
        case errors.Is(pe.Err, syscall.ELOOP):
            return handleSymlinkLoop(pe.Path)
        }
    }
    return err
}

Prevention

When it happens

Trigger: os.Stat on <dir>/<target> fails for any reason: file absent (ENOENT), permission denied (EACCES), path component is a file (ENOTDIR), symlink loop (ELOOP), or I/O error (EIO).

Common situations: Searching for config files (e.g. .crush.json) up a tree where some levels legitimately lack them — the ENOENT form is filtered upstream and harmless; EACCES on restrictive directories; ELOOP from `git worktree` + symlink tricks.

Related errors


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