{"record":{"id":"b6d3800da04db1d4","repo":"unslothai/unsloth","slug":"refusing-to-delete-storage-root","errorCode":null,"errorMessage":"Refusing to delete storage root","messagePattern":"Refusing to delete storage root","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"studio/backend/routes/models.py","lineNumber":2957,"sourceCode":"                raise HTTPException(\n                    status_code = 400,\n                    detail = \"Model path is outside Unsloth storage\",\n                )\n        else:\n            target_path = delete_path\n    else:\n        target_path = target_path.resolve()\n\n    should_check_resolved_path = not delete_path_is_symlink or (\n        export_type == \"gguf\" and gguf_variant\n    )\n    if should_check_resolved_path and not _is_path_under(target_path, allowed_root):\n        raise HTTPException(\n            status_code = 400,\n            detail = \"Model path is outside Unsloth storage\",\n        )\n    if target_path == allowed_root:\n        raise HTTPException(\n            status_code = 400,\n            detail = \"Refusing to delete storage root\",\n        )\n    if not target_path.exists() and not target_path.is_symlink():\n        raise HTTPException(status_code = 404, detail = \"Model not found on disk\")\n\n    if source == \"training\":\n        try:\n            from core.training import get_training_backend\n\n            training_backend = get_training_backend()\n            if training_backend.is_training_active():\n                raise HTTPException(\n                    status_code = 409,\n                    detail = \"Cannot delete trained models while training is running\",\n                )\n            # The diffusion (Images) trainer is a second independent run on the same storage root, so\n            # checking only the LLM backend let a delete rmtree a live run's output directory.","sourceCodeStart":2939,"sourceCodeEnd":2975,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/routes/models.py#L2939-L2975","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Target a specific run or export directory, never the root: e.g. outputs/run-42 or exports/model-gguf.","Client-side, assert the resolved path is a strict descendant: it must have the root as a parent AND differ from it.","For bulk cleanup, loop over scanned entries (each is a strict descendant) instead of deleting the root.","Check you did not append '..' or send '.' as the path."],"exampleFix":"# before\nrequests.delete('/delete-finetuned', json={'model_path': str(outputs_root()), 'source': 'training'})\n# after\nfor run in outputs_root().iterdir():\n    if run.is_dir():\n        requests.delete('/delete-finetuned', json={'model_path': str(run), 'source': 'training'})","handlingStrategy":"validation","validationCode":"root = outputs_root().resolve() if source == 'training' else exports_root().resolve()\ntarget = Path(model_path).expanduser().resolve()\nassert target != root and root in target.parents, 'refusing: target is the storage root'","typeGuard":"def is_specific_model_path(model_path: str, source: str) -> bool:\n    root = (outputs_root() if source == 'training' else exports_root()).resolve()\n    t = Path(model_path).expanduser().resolve()\n    return t != root and root in t.parents","tryCatchPattern":null,"preventionTips":["Never derive model_path with dirname()/os.path.dirname one level above a known model file.","Bulk-delete by iterating scanned entries, never by naming the root.","Strip trailing '/..' or '.' components before sending."],"tags":["fastapi","unsloth","destructive-operation","guard","filesystem","model-deletion"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}