{"record":{"id":"cf54f928f00933a8","repo":"BerriAI/litellm","slug":"git-subdir-path-must-be-a-relative-path-of-the-f","errorCode":null,"errorMessage":"git-subdir 'path' must be a relative path of the form 'segment/segment' (alphanumeric, dots, hyphens, underscores only)","messagePattern":"git-subdir 'path' must be a relative path of the form 'segment/segment' \\(alphanumeric, dots, hyphens, underscores only\\)","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py","lineNumber":195,"sourceCode":"            raise HTTPException(\n                status_code=400,\n                detail={\"error\": \"URL source must include 'url' field (e.g., 'https://github.com/org/repo.git')\"},\n            )\n    elif source_type == \"git-subdir\":\n        if not source.get(\"url\"):\n            raise HTTPException(\n                status_code=400,\n                detail={\n                    \"error\": \"git-subdir source must include 'url' field (e.g., 'https://github.com/org/repo.git')\"\n                },\n            )\n        if not source.get(\"path\"):\n            raise HTTPException(\n                status_code=400,\n                detail={\"error\": \"git-subdir source must include 'path' field (e.g., 'plugins/plugin-name')\"},\n            )\n        if not _VALID_GIT_SUBDIR_PATH_RE.match(source[\"path\"]):\n            raise HTTPException(\n                status_code=400,\n                detail={\n                    \"error\": \"git-subdir 'path' must be a relative path of the form 'segment/segment' (alphanumeric, dots, hyphens, underscores only)\"\n                },\n            )\n    else:\n        raise HTTPException(\n            status_code=400,\n            detail={\"error\": \"source.source must be 'github', 'url', or 'git-subdir'\"},\n        )\n\n\ndef _build_plugin_manifest(name: str, spec: PluginSpec) -> Mapping[str, object]:\n    \"\"\"Build the stored manifest dict shared by plugin create and update.\"\"\"\n    dumped: Final[Mapping[str, object]] = spec.model_dump(exclude_none=True)\n    return {\"name\": name, **{key: value for key, value in dumped.items() if value and key != \"name\"}}\n\n","sourceCodeStart":177,"sourceCodeEnd":213,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py#L177-L213","documentation":"400 thrown when a git-subdir source's \"path\" fails the allowlist regex ^[a-zA-Z0-9][a-zA-Z0-9._-]*(/[a-zA-Z0-9][a-zA-Z0-9._-]*)*$ — one or more '/'-separated segments, each starting alphanumeric and containing only alphanumerics, dots, hyphens, underscores. This is a security guard: it blocks path traversal ('..'), absolute paths, backslashes, and percent-encoded sequences before the path is ever used against a git checkout.","triggerScenarios":"Registering a plugin with path values like \"../other-plugin\", \"/plugins/my-plugin\", \"..\\\\plugins\\\\my-plugin\", \"%2e%2e/plugins\", \"plugins//my-plugin\" (empty segment), or \".hidden/plugin\" (segment starting with a dot) — all fail the regex and get this 400.","commonSituations":"Pointing the path outside the intended directory (traversal attempts or copy-pasted absolute paths from a local checkout); Windows-style backslash paths; URL-encoded paths passed through a client that pre-encodes; leading-dot directories like '.github'.","solutions":["Use a plain relative path of allowed segments, e.g. \"plugins/my-plugin\" or \"packages/pkg.name\".","Remove any leading '/', '..', backslashes, or percent-encoding before sending; strip and re-join the path client-side if it comes from user input.","If the real plugin directory starts with a dot or other disallowed character, rename the directory in the repo rather than fighting the allowlist."],"exampleFix":"# before\n{\"source\": {\"source\": \"git-subdir\", \"url\": \"https://github.com/org/repo.git\", \"path\": \"../secrets\"}}\n\n# after\n{\"source\": {\"source\": \"git-subdir\", \"url\": \"https://github.com/org/repo.git\", \"path\": \"plugins/plugin-name\"}}","handlingStrategy":"validation","validationCode":"import re\nVALID = re.compile(r\"^[a-zA-Z0-9][a-zA-Z0-9._-]*(/[a-zA-Z0-9][a-zA-Z0-9._-]*)*$\")\n\npath = source[\"path\"]\npath = path.replace(\"\\\\\", \"/\")\nif not VALID.match(path) or \"..\" in path.split(\"/\"):\n    raise ValueError(f\"git-subdir path {path!r} not allowed; use 'segment/segment' form\")\nsource[\"path\"] = path","typeGuard":"const GIT_SUBDIR_PATH = /^[a-zA-Z0-9][a-zA-Z0-9._-]*(\\/[a-zA-Z0-9][a-zA-Z0-9._-]*)*$/;\nfunction isSafeSubdirPath(p: unknown): p is string {\n  return typeof p === 'string' && GIT_SUBDIR_PATH.test(p) && !p.split('/').includes('..');\n}","tryCatchPattern":null,"preventionTips":["Mirror the server's segment allowlist client-side and normalize backslashes/URL-decoding before sending.","Reject any path containing '..', leading '/', or percent signs as a hard error in your tooling.","Keep plugin directories named with plain alphanumerics, dots, hyphens, underscores."],"tags":["litellm-proxy","claude-code-marketplace","validation","path-traversal","security","http-400"],"backgroundTag":"request-validation-failed","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}