unslothai/unsloth · error · HTTPException
Model path is outside Unsloth storage
Error message
Model path is outside Unsloth storage
What it means
Fires when the path to delete is a symlink and even its unresolved (lexical) absolute form does not live under the allowed storage root — outputs_root() for source="training", exports_root() for source="exported" (studio/backend/utils/paths/storage_roots.py:90-96). The endpoint only ever deletes inside Unsloth-managed directories, and symlinks are checked lexically first (via _is_path_under_lexically, models.py:2749) so a link pointing at /etc or the user's home is rejected before resolution. This is a path-containment / anti-traversal guard.
Source
Thrown at studio/backend/routes/models.py:2932
raw_path = Path(model_path).expanduser()
if source == "training":
target_path = raw_path
allowed_root = outputs_root()
else:
allowed_root = exports_root()
target_path = (
raw_path.parent
if export_type == "gguf" and raw_path.suffix.lower() == ".gguf"
else raw_path
)
allowed_root = allowed_root.resolve()
delete_path = Path(os.path.abspath(str(target_path)))
delete_path_is_symlink = delete_path.is_symlink()
if delete_path_is_symlink:
if not _is_path_under_lexically(delete_path, allowed_root):
raise HTTPException(
status_code = 400,
detail = "Model path is outside Unsloth storage",
)
if export_type == "gguf" and gguf_variant:
target_path = delete_path.resolve()
if not _is_path_under(target_path, allowed_root):
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
)View on GitHub (pinned to 203007d190)
Solutions
- Use the exact model_path shown by the studio models list for that row (it always comes from a scan of the correct root).
- Check that source matches where the model lives: "training" -> outputs_root(), "exported" -> exports_root().
- If you relocated storage, fix the roots (UNSLOTH_OUTPUTS_DIR / equivalent env or settings that outputs_root()/exports_root() read) rather than symlinking from outside.
- Verify containment yourself before calling: Path(model_path).expanduser().absolute() must start with the matching root.
Example fix
# before
requests.delete('/delete-finetuned', json={'model_path': '/etc/my-model', 'source': 'training'})
# after
root = Path(os.environ['UNSLOTH_OUTPUTS_DIR']).resolve()
target = Path(model_path).expanduser().absolute()
assert target == root or root in target.parents, 'outside storage'
requests.delete('/delete-finetuned', json={'model_path': str(target), 'source': 'training'}) Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def assert_delete_allowed(model_path: str, source: str, root: Path) -> None:
lex = Path(os.path.abspath(str(Path(model_path).expanduser())))
if lex != root and root not in lex.parents:
raise ValueError(f'{model_path} is outside {root} (lexical symlink check)') Type guard
def is_inside_storage(model_path: str, root: Path) -> bool:
p = Path(os.path.abspath(str(Path(model_path).expanduser())))
return p == root or root in p.parents Try / catch
try: api_delete(payload) except HTTPError as e: if e.response.status_code == 400 and 'outside Unsloth storage' in e.response.json()['detail']: log_and_reselect_path() else: raise
Prevention
- Always source model_path from the studio scan endpoint for the matching source value.
- Never symlink models into storage from elsewhere; move data or reconfigure the storage roots instead.
- Client-side, mirror the lexical containment check before every delete call.
When it happens
Trigger: model_path = "/etc/passwd" with source="training"; model_path = "~/.cache/huggingface/..."; a symlink like outputs/link -> ../../secrets whose abspath spelling itself escapes the root; source set to "training" while passing a path that lives under the exports root (wrong root for that source).
Common situations: Users hand-crafting model_path instead of copying it from the studio UI/scan results; moving or re-pointing an outputs directory with symlinks after a disk change; mixing up source="training" vs "exported" so the wrong root is enforced; attempts to use the delete endpoint as a general file-deletion utility.
Related errors
- Path is not in the browseable allowlist. Register it via POS
- Refusing to delete storage root
- GGUF variant deletion requires an export directory
- Dataset '{cleaned}' must not be a symbolic link.
- Dataset '{cleaned}' escapes the Studio datasets directory.
AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15).
Data as JSON: /api/errors/3bf138b5c54a5f33.
Report an issue: GitHub.