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

  1. Use an absolute POSIX path (/home/user/models), a ~ path (~/models), or a Windows drive path (C:\\models or D:/models)
  2. Replace $HOME with ~
  3. Remove shell metacharacters, command substitutions, and unmatched quotes from the path
  4. 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

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


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/47fb7663646319a0. Report an issue: GitHub.