{"record":{"id":"ef612cb4583c14c9","repo":"odysseus-dev/odysseus","slug":"invalid-local-dir-path-segments-cannot-start-wit","errorCode":null,"errorMessage":"Invalid local_dir — path segments cannot start with '-'","messagePattern":"Invalid local_dir — path segments cannot start with '-'","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"routes/cookbook_helpers.py","lineNumber":128,"sourceCode":"    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\n\ndef _shell_path(p: str) -> str:\n    \"\"\"Render a validated path for a double-quoted shell context, expanding a\n    leading ~ to $HOME (single quotes wouldn't expand it). Safe because\n    _validate_local_dir already rejects quotes and shell metacharacters.\"\"\"\n    if p == \"~\":\n        return '\"$HOME\"'\n    if p.startswith(\"~/\"):","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/cookbook_helpers.py#L110-L146","documentation":"HTTP 400 from _validate_local_dir() in routes/cookbook_helpers.py. After the path passes the character allowlist, each segment (split on / and backslash) is checked for a leading '-'. A segment like -rf could be parsed as a CLI flag by hf/vllm/etc. when the path is embedded in a command — quoting does not prevent option parsing, so the guard lives in the validator itself (see the in-code comment).","triggerScenarios":"local_dir=\"/models/-rf\", local_dir=\"D:\\\\models\\\\-opt\", or any directory whose name begins with a hyphen such as \"~/models/-checkpoint\".","commonSituations":"User-created directories that happen to start with '-' (some tools generate '-checkpoint' style names); attempts at option injection through the path field.","solutions":["Rename the directory so no segment starts with '-' (e.g. ~/models/checkpoint instead of ~/models/-checkpoint)","If you genuinely need such a name, keep it outside local_dir and symlink to it from a safe name"],"exampleFix":"// before\n{\"local_dir\": \"/data/-snapshot\"}\n// after\n{\"local_dir\": \"/data/snapshot\"}","handlingStrategy":"validation","validationCode":"import re\n\ndef no_dash_segments(path: str) -> bool:\n    return not any(seg.startswith(\"-\") for seg in re.split(r\"[\\\\/]\", path) if seg)\n\nif not no_dash_segments(local_dir):\n    raise ValueError(\"rename directory: segments may not start with '-'\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Name model directories without a leading hyphen","Treat '-'-prefixed dirs as an option-injection red flag","Re-run the full _validate_local_dir mirror (see error 366) client-side"],"tags":["validation","option-injection","path","http-400","security"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}