{"record":{"id":"b353934e72789425","repo":"BerriAI/litellm","slug":"plugin-name-must-be-kebab-case-lowercase-letters","errorCode":null,"errorMessage":"Plugin name must be kebab-case (lowercase letters, numbers, hyphens)","messagePattern":"Plugin name must be kebab-case \\(lowercase letters, numbers, hyphens\\)","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py","lineNumber":277,"sourceCode":"        ```bash\n        curl -X POST http://localhost:4000/claude-code/plugins \\\\\n          -H \"Authorization: Bearer sk-...\" \\\\\n          -H \"Content-Type: application/json\" \\\\\n          -d '{\n            \"name\": \"my-plugin\",\n            \"source\": {\"source\": \"github\", \"repo\": \"org/my-plugin\"},\n            \"version\": \"1.0.0\",\n            \"description\": \"My awesome plugin\"\n          }'\n        ```\n    \"\"\"\n    from prisma.errors import UniqueViolationError\n\n    try:\n        prisma_client: Final = await _get_prisma_client()\n\n        if not re.match(r\"^[a-z0-9-]+$\", request.name):\n            raise HTTPException(\n                status_code=400,\n                detail={\"error\": \"Plugin name must be kebab-case (lowercase letters, numbers, hyphens)\"},\n            )\n\n        _validate_plugin_source(request.source)\n\n        existing: Final[_PluginRecord | None] = await ClaudeCodePluginRepository(prisma_client).table.find_unique(\n            where={\"name\": request.name}\n        )\n        if existing:\n            raise _name_conflict_error(request.name)\n\n        manifest: Final[Mapping[str, object]] = _build_plugin_manifest(request.name, request)\n\n        try:\n            plugin: Final[_PluginRecord] = await ClaudeCodePluginRepository(prisma_client).table.create(\n                data={\n                    \"name\": request.name,","sourceCodeStart":259,"sourceCodeEnd":295,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py#L259-L295","documentation":"400 from register_plugin: the plugin name must match ^[a-z0-9-]+$ — kebab-case only (lowercase letters, digits, hyphens). The name is used as the primary key/unique identifier in litellm_claudecodeplugintable and as the marketplace identifier, so LiteLLM enforces the restrictive format up front, before source validation and the uniqueness check.","triggerScenarios":"POST /claude-code/plugins with names like \"My_Plugin\" (uppercase + underscore), \"my plugin\" (space), \"my.plugin\" (dot), \"plugin--name!!\", or an empty string — anything containing characters outside [a-z0-9-].","commonSituations":"Reusing a display name or npm-style package name (my_plugin) as the plugin id; auto-generating names from titles without slugifying; trailing whitespace from form input.","solutions":["Slugify the name to lowercase kebab-case: \"my-plugin\".","Strip/replace disallowed characters before sending: lowercase, spaces and underscores to hyphens, drop dots and symbols.","If updating an existing plugin, note the name is also the route parameter — use the same kebab-case name in PUT /claude-code/plugins/{name}."],"exampleFix":"# before\ncurl -X POST http://localhost:4000/claude-code/plugins \\\n  -d '{\"name\": \"Data_Cleaner\", \"source\": {\"source\": \"github\", \"repo\": \"org/dc\"}}'\n\n# after\ncurl -X POST http://localhost:4000/claude-code/plugins \\\n  -d '{\"name\": \"data-cleaner\", \"source\": {\"source\": \"github\", \"repo\": \"org/dc\"}}'","handlingStrategy":"validation","validationCode":"import re\n\ndef slugify(name: str) -> str:\n    s = re.sub(r\"[^a-zA-Z0-9]+\", \"-\", name.strip().lower()).strip(\"-\")\n    if not re.fullmatch(r\"[a-z0-9-]+\", s):\n        raise ValueError(f\"cannot slugify {name!r} to kebab-case\")\n    return s\n\npayload[\"name\"] = slugify(payload[\"name\"])  # 'Data_Cleaner' -> 'data-cleaner'","typeGuard":"const KEBAB = /^[a-z0-9-]+$/;\nfunction isKebabName(n: unknown): n is string {\n  return typeof n === 'string' && n.length > 0 && KEBAB.test(n);\n}","tryCatchPattern":null,"preventionTips":["Always slugify display names before registration; store the mapping display-name -> slug.","Use the same slug verbatim in every /claude-code/plugins/{name} route call."],"tags":["litellm-proxy","claude-code-marketplace","validation","naming","http-400","plugin-registration"],"backgroundTag":"request-validation-failed","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}