charmbracelet/crush · error

cannot convert stop dir to absolute path: %w

Error message

cannot convert stop dir to absolute path: %w

What it means

When stopDir is non-empty, traverseUpBounded resolves it with filepath.Abs so the bounded walk can compare canonicalized paths. This error wraps a failure of that resolution, again only possible via os.Getwd when stopDir is relative.

Source

Thrown at internal/fsext/lookup.go:215

// traverseUp instead. If stopDir is set but is not an ancestor of dir
// the walk still stops at the filesystem root, so callers cannot
// accidentally produce an infinite walk by passing a sibling path.
//
// Boundary comparison is performed against symlink-resolved paths so
// that callers passing logically equivalent paths (a symlinked /var vs
// the underlying /private/var, for example) still terminate at the
// expected directory.
func traverseUpBounded(dir, stopDir string, walkFn func(dir string, owner int) error) error {
	cwd, err := filepath.Abs(dir)
	if err != nil {
		return fmt.Errorf("cannot convert CWD to absolute path: %w", err)
	}

	stop := cwd
	if stopDir != "" {
		stop, err = filepath.Abs(stopDir)
		if err != nil {
			return fmt.Errorf("cannot convert stop dir to absolute path: %w", err)
		}
	}
	canonStop := canonicalize(stop)

	owner, err := Owner(dir)
	if err != nil {
		return fmt.Errorf("cannot get ownership: %w", err)
	}

	for {
		err := walkFn(cwd, owner)
		if err == nil || errors.Is(err, filepath.SkipDir) {
			if canonicalize(cwd) == canonStop {
				return nil
			}

			parent := filepath.Dir(cwd)
			if parent == cwd {

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Pass an absolute stopDir (e.g. the resolved project root) to avoid Getwd dependency
  2. Ensure the process CWD is valid before the call, or chdir to a safe directory
  3. Pre-resolve the stop dir yourself with filepath.Abs and handle the error at a more meaningful layer
  4. Restart the process from a valid directory

Example fix

// before
fsext.LookupBounded(dir, "../../project", targets...)
// after
root, err := filepath.Abs("/workspaces/project")
if err != nil {
    return err
}
fsext.LookupBounded(dir, root, targets...)
Defensive patterns

Strategy: validation

Validate before calling

absStop, err := filepath.Abs(stopDir)
if err != nil {
    return fmt.Errorf("stop dir %q not resolvable: %w", stopDir, err)
}
if _, err := os.Stat(absStop); err != nil {
    return fmt.Errorf("stop dir unavailable: %w", err)
}

Type guard

func stopDirResolvable(stopDir string) bool {
    abs, err := filepath.Abs(stopDir)
    return err == nil && func() bool { _, err := os.Stat(abs); return err == nil }()
}

Try / catch

_, err := fsext.LookupBounded(dir, stop, targets...)
if err != nil {
    if strings.Contains(err.Error(), "cannot convert stop dir") {
        return fmt.Errorf("bad stop dir %q: %w", stop, err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling LookupBounded/LookupClosestBounded with a relative stopDir (e.g. ".." or "./project") while the process working directory cannot be determined — deleted CWD, permission loss, or failed mount.

Common situations: Computing the project root as a relative path and passing it straight through; running from a removed build directory; containers with dangling CWD.

Related errors


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