{"record":{"id":"f3e3e438c25389a9","repo":"usestrix/strix","slug":"path-is-not-an-existing-directory","errorCode":null,"errorMessage":"'{path}' is not an existing directory.","messagePattern":"'(.+?)' is not an existing directory\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"strix/interface/utils.py","lineNumber":1388,"sourceCode":"        \".docker\",\n        \".config\",\n        \".npm\",\n        \".pki\",\n        \".terraform.d\",\n    }\n)\n\n\ndef _is_within(path: Path, ancestor: Path) -> bool:\n    ancestor_parts = [part.casefold() for part in ancestor.parts]\n    path_parts = [part.casefold() for part in path.parts]\n    return path_parts[: len(ancestor_parts)] == ancestor_parts\n\n\ndef check_mountable_dir(path: Path) -> None:\n    resolved = path.resolve()\n    if not resolved.is_dir():\n        raise ValueError(f\"'{path}' is not an existing directory.\")\n\n    # Both the literal and the resolved form: macOS reaches /etc through the\n    # /private/etc symlink, and only the resolved path is compared below.\n    exact = {str(Path(root)).casefold() for root in _FORBIDDEN_MOUNT_ROOTS}\n    exact |= {str(Path(root).resolve()).casefold() for root in _FORBIDDEN_MOUNT_ROOTS}\n    exact.add(str(Path.home().resolve()).casefold())\n    tree_roots = set(_FORBIDDEN_MOUNT_TREES)\n    if os.name == \"nt\":\n        drive = Path(resolved.anchor)\n        tree_roots |= {str(drive / name) for name in _FORBIDDEN_WINDOWS_TREE_NAMES}\n        exact.add(str(drive / \"Users\").casefold())\n    trees = [Path(root) for root in tree_roots] + [Path(root).resolve() for root in tree_roots]\n    if (\n        str(resolved).casefold() in exact\n        or resolved.parent == resolved\n        or any(_is_within(resolved, tree) for tree in trees)\n    ):\n        raise ValueError(","sourceCodeStart":1370,"sourceCodeEnd":1406,"githubUrl":"https://github.com/usestrix/strix/blob/85513391305171ecc6faffe03da4a8bda5e3febb/strix/interface/utils.py#L1370-L1406","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Pass a directory that exists at call time: Path(target).resolve().is_dir() must be True.","If using symlinked release dirs, point at a stable path or re-create the symlink before scanning.","Target the project folder, not a file inside it."],"exampleFix":"# before\ncheck_mountable_dir(Path(\"./current\"))   # dangling symlink\n\n# after\ncheck_mountable_dir(Path(\"/srv/app/releases/2026-08-14\").resolve())","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\nresolved = Path(target).resolve()\nif not resolved.is_dir():\n    raise SystemExit(f\"{target} resolves to {resolved}, which is not an existing directory\")\ncheck_mountable_dir(resolved)","typeGuard":"def is_existing_directory(path: str | Path) -> bool:\n    try:\n        return Path(path).resolve().is_dir()\n    except OSError:\n        return False","tryCatchPattern":"try:\n    check_mountable_dir(Path(target))\nexcept ValueError as e:\n    if \"is not an existing directory\" in str(e):\n        raise SystemExit(f\"Scan target must be a live directory: {target}\") from e\n    raise","preventionTips":["Resolve symlinks yourself before scanning so dangling links surface early","Avoid symlink-of-the-day directories that can rotate away mid-pipeline"],"tags":["filesystem","sandbox","docker","validation"],"backgroundTag":null,"analyzedSha":"85513391305171ecc6faffe03da4a8bda5e3febb","analyzedAt":"2026-08-15T05:03:57.275Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}