unslothai/unsloth · error · HTTPException

Refusing to delete storage root

Error message

Refusing to delete storage root

What it means

Refuses when the validated target path equals the storage root itself (target_path == allowed_root at models.py:2957). Deleting the root would wipe every trained and exported model at once, so even a well-formed request naming the root directory is rejected with 400. It usually means model_path was the bare root or resolved to it (e.g. "." inside the root, or a path of only dot-dot segments landing exactly on the root).

Source

Thrown at studio/backend/routes/models.py:2957

                raise HTTPException(
                    status_code = 400,
                    detail = "Model path is outside Unsloth storage",
                )
        else:
            target_path = delete_path
    else:
        target_path = target_path.resolve()

    should_check_resolved_path = not delete_path_is_symlink or (
        export_type == "gguf" and gguf_variant
    )
    if should_check_resolved_path and not _is_path_under(target_path, allowed_root):
        raise HTTPException(
            status_code = 400,
            detail = "Model path is outside Unsloth storage",
        )
    if target_path == allowed_root:
        raise HTTPException(
            status_code = 400,
            detail = "Refusing to delete storage root",
        )
    if not target_path.exists() and not target_path.is_symlink():
        raise HTTPException(status_code = 404, detail = "Model not found on disk")

    if source == "training":
        try:
            from core.training import get_training_backend

            training_backend = get_training_backend()
            if training_backend.is_training_active():
                raise HTTPException(
                    status_code = 409,
                    detail = "Cannot delete trained models while training is running",
                )
            # The diffusion (Images) trainer is a second independent run on the same storage root, so
            # checking only the LLM backend let a delete rmtree a live run's output directory.

View on GitHub (pinned to 203007d190)

Solutions

  1. Target a specific run or export directory, never the root: e.g. outputs/run-42 or exports/model-gguf.
  2. Client-side, assert the resolved path is a strict descendant: it must have the root as a parent AND differ from it.
  3. For bulk cleanup, loop over scanned entries (each is a strict descendant) instead of deleting the root.
  4. Check you did not append '..' or send '.' as the path.

Example fix

# before
requests.delete('/delete-finetuned', json={'model_path': str(outputs_root()), 'source': 'training'})
# after
for run in outputs_root().iterdir():
    if run.is_dir():
        requests.delete('/delete-finetuned', json={'model_path': str(run), 'source': 'training'})
Defensive patterns

Strategy: validation

Validate before calling

root = outputs_root().resolve() if source == 'training' else exports_root().resolve()
target = Path(model_path).expanduser().resolve()
assert target != root and root in target.parents, 'refusing: target is the storage root'

Type guard

def is_specific_model_path(model_path: str, source: str) -> bool:
    root = (outputs_root() if source == 'training' else exports_root()).resolve()
    t = Path(model_path).expanduser().resolve()
    return t != root and root in t.parents

Prevention

When it happens

Trigger: model_path = "/unsloth-outputs" with source="training"; model_path = "." when the server cwd is the root; model_path = "/exports/my-export/.." which resolves to /exports; UI 'delete all' feature mistakenly passing the root as a single path.

Common situations: Clients that derive model_path as dirname of a real path one level too high; scripts that normalize paths and strip the last component; calling the endpoint with the folder shown at the top of a file picker.

Related errors


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