usestrix/strix · error · ValueError

'{path}' is not an existing directory.

Error message

'{path}' is not an existing directory.

What it means

check_mountable_dir resolves the path and requires it to be an existing directory before the sandbox-mount safety checks. Unlike the is_dir() check in infer_target_type, this uses the RESOLVED path, so symlinks pointing at non-directories or paths removed between call and resolution fail here.

Source

Thrown at strix/interface/utils.py:1388

        ".docker",
        ".config",
        ".npm",
        ".pki",
        ".terraform.d",
    }
)


def _is_within(path: Path, ancestor: Path) -> bool:
    ancestor_parts = [part.casefold() for part in ancestor.parts]
    path_parts = [part.casefold() for part in path.parts]
    return path_parts[: len(ancestor_parts)] == ancestor_parts


def check_mountable_dir(path: Path) -> None:
    resolved = path.resolve()
    if not resolved.is_dir():
        raise ValueError(f"'{path}' is not an existing directory.")

    # Both the literal and the resolved form: macOS reaches /etc through the
    # /private/etc symlink, and only the resolved path is compared below.
    exact = {str(Path(root)).casefold() for root in _FORBIDDEN_MOUNT_ROOTS}
    exact |= {str(Path(root).resolve()).casefold() for root in _FORBIDDEN_MOUNT_ROOTS}
    exact.add(str(Path.home().resolve()).casefold())
    tree_roots = set(_FORBIDDEN_MOUNT_TREES)
    if os.name == "nt":
        drive = Path(resolved.anchor)
        tree_roots |= {str(drive / name) for name in _FORBIDDEN_WINDOWS_TREE_NAMES}
        exact.add(str(drive / "Users").casefold())
    trees = [Path(root) for root in tree_roots] + [Path(root).resolve() for root in tree_roots]
    if (
        str(resolved).casefold() in exact
        or resolved.parent == resolved
        or any(_is_within(resolved, tree) for tree in trees)
    ):
        raise ValueError(

View on GitHub (pinned to 8551339130)

Solutions

  1. Pass a directory that exists at call time: Path(target).resolve().is_dir() must be True.
  2. If using symlinked release dirs, point at a stable path or re-create the symlink before scanning.
  3. Target the project folder, not a file inside it.

Example fix

# before
check_mountable_dir(Path("./current"))   # dangling symlink

# after
check_mountable_dir(Path("/srv/app/releases/2026-08-14").resolve())
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

resolved = Path(target).resolve()
if not resolved.is_dir():
    raise SystemExit(f"{target} resolves to {resolved}, which is not an existing directory")
check_mountable_dir(resolved)

Type guard

def is_existing_directory(path: str | Path) -> bool:
    try:
        return Path(path).resolve().is_dir()
    except OSError:
        return False

Try / catch

try:
    check_mountable_dir(Path(target))
except ValueError as e:
    if "is not an existing directory" in str(e):
        raise SystemExit(f"Scan target must be a live directory: {target}") from e
    raise

Prevention

When it happens

Trigger: check_mountable_dir(Path('mylink')) where mylink resolves to a file or to a deleted path; a symlink chain where the final target no longer exists; calling the function directly with a file path.

Common situations: Passing a symlinked 'current' release directory whose target was rotated away; wrapper code re-validating a path after the directory was moved; passing a project file rather than its folder.

Related errors


AI-assisted analysis of usestrix/strix@8551339130 (2026-08-15). Data as JSON: /api/errors/f3e3e438c25389a9. Report an issue: GitHub.