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
- If the wrapped error is os.ErrNotExist or os.ErrPermission, it is expected and already skipped — no action needed
- For ENOTDIR, ensure no target-name component is a regular file in ancestors
- Break symlink loops (ELOOP) and check mount health for EIO
- 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
- Remember ENOENT/EACCES are filtered internally — only unusual errnos reach you
- errors.As with *os.PathError to recover path and errno
- Break symlink loops in search trees
- Don't shadow target names with regular files in ancestor dirs
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
- failed to access file: %w
- error accessing file: %w
- cannot get ownership: %w
- skill not found
- failed to create parent directories: %w
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/ddedb93fa9fd0780.
Report an issue: GitHub.