{"record":{"id":"47fb7663646319a0","repo":"odysseus-dev/odysseus","slug":"invalid-local-dir-must-be-an-absolute-or-path","errorCode":null,"errorMessage":"Invalid local_dir — must be an absolute or ~ path with no shell metacharacters","messagePattern":"Invalid local_dir — must be an absolute or ~ path with no shell metacharacters","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"routes/cookbook_helpers.py","lineNumber":120,"sourceCode":"            env = state.get(\"env\") if isinstance(state, dict) else {}\n            if isinstance(env, dict) and env.get(\"hfToken\"):\n                from src.secret_storage import decrypt\n                token = decrypt(env.get(\"hfToken\") or \"\")\n        except Exception:\n            token = \"\"\n    if not token:\n        token = (os.environ.get(\"HF_TOKEN\") or os.environ.get(\"HUGGING_FACE_HUB_TOKEN\") or \"\").strip()\n    return token\n\n\ndef _validate_local_dir(v: str | None) -> str | None:\n    if v is None or v == \"\":\n        return None\n    if len(v) >= 2 and v[0] == v[-1] and v[0] in {\"'\", '\"'}:\n        v = v[1:-1]\n    v = v.rstrip(\"/\") or \"/\"\n    if not (_LOCAL_DIR_RE.match(v) or _WINDOWS_LOCAL_DIR_RE.match(v)):\n        raise HTTPException(400, \"Invalid local_dir — must be an absolute or ~ path with no shell metacharacters\")\n    # Reject path segments that start with '-' (option injection). '-' is in the\n    # allowlist, so a dir like ``/models/-rf`` or ``D:\\models\\-rf`` could be read\n    # as a CLI flag by hf/etc. — and quoting does NOT stop a value from being\n    # parsed as an option. This is the one residual that command-build-time\n    # quoting can't cover, so the guard lives here, keeping the safety wholly\n    # inside the validator rather than relying on consumers.\n    if any(seg.startswith(\"-\") for seg in re.split(r\"[\\\\/]\", v) if seg):\n        raise HTTPException(400, \"Invalid local_dir — path segments cannot start with '-'\")\n    return v\n\n\ndef _validate_gpus(v: str | None) -> str | None:\n    if v is None or v == \"\":\n        return None\n    if not _GPU_LIST_RE.fullmatch(str(v)):\n        raise HTTPException(400, \"Invalid gpus — expected comma-separated GPU indexes\")\n    return str(v)\n","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/cookbook_helpers.py#L102-L138","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\n{\"local_dir\": \"$HF_HOME/hub/models\"}\n// after\n{\"local_dir\": \"~/.cache/huggingface/hub/models\"}","handlingStrategy":"validation","validationCode":"import re\nLOCAL_DIR = re.compile(r\"^(/[^/]*(/[A-Za-z0-9._@+-]+)*/?|~(/[A-Za-z0-9._@+-]+)*/?)$\")\nWIN_DIR = re.compile(r\"^[A-Za-z]:[\\\\/][A-Za-z0-9._@+-]+([\\\\/][A-Za-z0-9._@+-]+)*[\\\\/]?$\")\n\ndef prevalidate_local_dir(v):\n    if v is None or v == \"\":\n        return None\n    if len(v) >= 2 and v[0] == v[-1] and v[0] in \"'\\\"\":\n        v = v[1:-1]\n    v = v.rstrip(\"/\") or \"/\"\n    if not (LOCAL_DIR.match(v) or WIN_DIR.match(v)):\n        raise ValueError(\"local_dir must be absolute or ~, no shell metacharacters\")\n    if any(s.startswith(\"-\") for s in re.split(r\"[\\\\/]\", v) if s):\n        raise ValueError(\"path segment starts with '-'\")\n    return v","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always send absolute or ~-based paths","Use ~ instead of $HOME","Avoid spaces and metacharacters in model directories"],"tags":["validation","path","shell-injection","http-400"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}