{"record":{"id":"fb5e8963ea9021fd","repo":"unslothai/unsloth","slug":"directory-not-allowed","errorCode":null,"errorMessage":"Directory not allowed","messagePattern":"Directory not allowed","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"studio/backend/hub/services/models/local_inventory.py","lineNumber":896,"sourceCode":"\n\nasync def _scan_local_models_response(\n    models_dir: str, custom_folders: list[dict], sources: _LocalInventorySources\n) -> LocalModelListResponse:\n    \"\"\"List local model candidates from every supported on-device source.\"\"\"\n    hf_cache_dir, legacy_hf, hf_default, lm_dirs, ollama_dirs, known_hf_caches = sources\n\n    allowed_roots: list[Path] = [Path(\"./models\").resolve(), hf_cache_dir]\n    if _safe_is_dir(legacy_hf):\n        allowed_roots.append(legacy_hf)\n    if _safe_is_dir(hf_default):\n        allowed_roots.append(hf_default)\n    allowed_roots.extend([studio_root(), outputs_root()])\n\n    try:\n        models_root = _resolve_allowed_models_dir(models_dir, allowed_roots)\n    except ValueError:\n        raise HTTPException(status_code = 403, detail = \"Directory not allowed\")\n\n    try:\n        local_models = await _collect_models_from_default_sources(\n            models_root,\n            hf_cache_dir,\n            legacy_hf,\n            hf_default,\n            lm_dirs,\n            ollama_dirs,\n            known_hf_caches,\n            custom_folders,\n        )\n        models = _dedupe_local_models(_filter_hidden_models(local_models))\n        return LocalModelListResponse(\n            models_dir = str(models_root),\n            hf_cache_dir = str(hf_cache_dir),\n            lmstudio_dirs = [str(d) for d in lm_dirs],\n            ollama_dirs = [str(d) for d in ollama_dirs],","sourceCodeStart":878,"sourceCodeEnd":914,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/hub/services/models/local_inventory.py#L878-L914","documentation":"HTTP 403 returned by the local-models listing endpoint when _resolve_allowed_models_dir rejects the requested models_dir. The resolver confines models_dir to an allowlist: ./models, the HF cache dir, the legacy HF dir, the default HF dir, the studio root, and the outputs root. Any directory outside that set is refused so the endpoint cannot be used as a general filesystem browser.","triggerScenarios":"Calling GET /api/models/local with models_dir=/home/alice/my-models (or any path outside the allowed roots); a symlinked models_dir whose realpath escapes the allowed roots after normalization.","commonSituations":"Front-end state persisting an old models_dir after the cache moved; scripts hitting the API directly with an arbitrary path; users expecting to browse any folder — they must instead register it via the scan-folders API which has its own policy.","solutions":["Omit models_dir to use the default ./models, or set it to the active HF cache directory.","Move/symlink the desired folder under an allowed root (e.g. ln -s /data/models ./models/my-models).","For arbitrary folders, use the scan-folders registration endpoint (add_scan_folder) rather than models_dir, after confirming the folder passes its denylist."],"exampleFix":"# before\nGET /api/models/local?models_dir=/data/llama\n# after\nGET /api/models/local?models_dir=/root/project/models   # or register /data/llama as a scan folder","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef is_allowed_models_dir(p: str, allowed_roots: list[Path]) -> bool:\n    try:\n        real = Path(p).resolve()\n    except (OSError, ValueError):\n        return False\n    return any(\n        real == root.resolve() or root.resolve() in real.parents\n        for root in allowed_roots\n    )","typeGuard":null,"tryCatchPattern":"resp = client.get(\"/api/models/local\", params={\"models_dir\": d})\nif resp.status_code == 403:\n    # policy refusal, not a bug: switch to default dir or register a scan folder","preventionTips":["Treat models_dir as a curated knob, not a free-form browser.","Register arbitrary folders through the scan-folders API instead of models_dir.","Remember symlinks are resolved — a link escaping the allowed roots is still denied."],"tags":["http-403","authorization","local-models","path-policy"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}