unslothai/unsloth · error · HTTPException

Directory not allowed

Error message

Directory not allowed

What it means

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.

Source

Thrown at studio/backend/hub/services/models/local_inventory.py:896


async def _scan_local_models_response(
    models_dir: str, custom_folders: list[dict], sources: _LocalInventorySources
) -> LocalModelListResponse:
    """List local model candidates from every supported on-device source."""
    hf_cache_dir, legacy_hf, hf_default, lm_dirs, ollama_dirs, known_hf_caches = sources

    allowed_roots: list[Path] = [Path("./models").resolve(), hf_cache_dir]
    if _safe_is_dir(legacy_hf):
        allowed_roots.append(legacy_hf)
    if _safe_is_dir(hf_default):
        allowed_roots.append(hf_default)
    allowed_roots.extend([studio_root(), outputs_root()])

    try:
        models_root = _resolve_allowed_models_dir(models_dir, allowed_roots)
    except ValueError:
        raise HTTPException(status_code = 403, detail = "Directory not allowed")

    try:
        local_models = await _collect_models_from_default_sources(
            models_root,
            hf_cache_dir,
            legacy_hf,
            hf_default,
            lm_dirs,
            ollama_dirs,
            known_hf_caches,
            custom_folders,
        )
        models = _dedupe_local_models(_filter_hidden_models(local_models))
        return LocalModelListResponse(
            models_dir = str(models_root),
            hf_cache_dir = str(hf_cache_dir),
            lmstudio_dirs = [str(d) for d in lm_dirs],
            ollama_dirs = [str(d) for d in ollama_dirs],

View on GitHub (pinned to 203007d190)

Solutions

  1. Omit models_dir to use the default ./models, or set it to the active HF cache directory.
  2. Move/symlink the desired folder under an allowed root (e.g. ln -s /data/models ./models/my-models).
  3. For arbitrary folders, use the scan-folders registration endpoint (add_scan_folder) rather than models_dir, after confirming the folder passes its denylist.

Example fix

# before
GET /api/models/local?models_dir=/data/llama
# after
GET /api/models/local?models_dir=/root/project/models   # or register /data/llama as a scan folder
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def is_allowed_models_dir(p: str, allowed_roots: list[Path]) -> bool:
    try:
        real = Path(p).resolve()
    except (OSError, ValueError):
        return False
    return any(
        real == root.resolve() or root.resolve() in real.parents
        for root in allowed_roots
    )

Try / catch

resp = client.get("/api/models/local", params={"models_dir": d})
if resp.status_code == 403:
    # policy refusal, not a bug: switch to default dir or register a scan folder

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/fb5e8963ea9021fd. Report an issue: GitHub.