odysseus-dev/odysseus · error · HTTPException
Invalid local_dir — must be an absolute or ~ path with no sh
Error message
Invalid local_dir — must be an absolute or ~ path with no shell metacharacters
What it means
HTTP 400 from _validate_local_dir() in routes/cookbook_helpers.py when local_dir matches neither _LOCAL_DIR_RE (POSIX absolute or ~ path) nor _WINDOWS_LOCAL_DIR_RE (drive-letter path). This is an anti-shell-injection guard: relative paths and shell metacharacters are forbidden because the value is later interpolated into shell commands via _shell_path(). Surrounding matching quotes are stripped and a trailing / normalized before matching.
Source
Thrown at routes/cookbook_helpers.py:120
env = state.get("env") if isinstance(state, dict) else {}
if isinstance(env, dict) and env.get("hfToken"):
from src.secret_storage import decrypt
token = decrypt(env.get("hfToken") or "")
except Exception:
token = ""
if not token:
token = (os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN") or "").strip()
return token
def _validate_local_dir(v: str | None) -> str | None:
if v is None or v == "":
return None
if len(v) >= 2 and v[0] == v[-1] and v[0] in {"'", '"'}:
v = v[1:-1]
v = v.rstrip("/") or "/"
if not (_LOCAL_DIR_RE.match(v) or _WINDOWS_LOCAL_DIR_RE.match(v)):
raise HTTPException(400, "Invalid local_dir — must be an absolute or ~ path with no shell metacharacters")
# Reject path segments that start with '-' (option injection). '-' is in the
# allowlist, so a dir like ``/models/-rf`` or ``D:\models\-rf`` could be read
# as a CLI flag by hf/etc. — and quoting does NOT stop a value from being
# parsed as an option. This is the one residual that command-build-time
# quoting can't cover, so the guard lives here, keeping the safety wholly
# inside the validator rather than relying on consumers.
if any(seg.startswith("-") for seg in re.split(r"[\\/]", v) if seg):
raise HTTPException(400, "Invalid local_dir — path segments cannot start with '-'")
return v
def _validate_gpus(v: str | None) -> str | None:
if v is None or v == "":
return None
if not _GPU_LIST_RE.fullmatch(str(v)):
raise HTTPException(400, "Invalid gpus — expected comma-separated GPU indexes")
return str(v)
View on GitHub (pinned to f9235ebbf1)
Solutions
- Use an absolute POSIX path (/home/user/models), a ~ path (~/models), or a Windows drive path (C:\\models or D:/models)
- Replace $HOME with ~
- Remove shell metacharacters, command substitutions, and unmatched quotes from the path
- Prefer forward slashes on Windows; both forms are accepted but forward slashes avoid double-escape bugs
Example fix
// before
{"local_dir": "$HF_HOME/hub/models"}
// after
{"local_dir": "~/.cache/huggingface/hub/models"} Defensive patterns
Strategy: validation
Validate before calling
import re
LOCAL_DIR = re.compile(r"^(/[^/]*(/[A-Za-z0-9._@+-]+)*/?|~(/[A-Za-z0-9._@+-]+)*/?)$")
WIN_DIR = re.compile(r"^[A-Za-z]:[\\/][A-Za-z0-9._@+-]+([\\/][A-Za-z0-9._@+-]+)*[\\/]?$")
def prevalidate_local_dir(v):
if v is None or v == "":
return None
if len(v) >= 2 and v[0] == v[-1] and v[0] in "'\"":
v = v[1:-1]
v = v.rstrip("/") or "/"
if not (LOCAL_DIR.match(v) or WIN_DIR.match(v)):
raise ValueError("local_dir must be absolute or ~, no shell metacharacters")
if any(s.startswith("-") for s in re.split(r"[\\/]", v) if s):
raise ValueError("path segment starts with '-'")
return v Prevention
- Always send absolute or ~-based paths
- Use ~ instead of $HOME
- Avoid spaces and metacharacters in model directories
When it happens
Trigger: local_dir="models/llama" (relative), local_dir="$HOME/models" (variable expansion instead of ~), local_dir="~/mode;ls" (metacharacter), local_dir="C:models" (no slash after drive).
Common situations: Users used to relative paths; putting env vars in the path; Windows paths with mixed separators; stray quote characters left over from JSON or shell quoting.
Related errors
- Invalid local_dir — path segments cannot start with '-'
- Invalid characters in cmd
- await res.text()
- {key} must be an integer
- Invalid JSON
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/47fb7663646319a0.
Report an issue: GitHub.