BerriAI/litellm · error · HTTPException
Plugin name must be kebab-case (lowercase letters, numbers,
Error message
Plugin name must be kebab-case (lowercase letters, numbers, hyphens)
What it means
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.
Source
Thrown at litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py:277
```bash
curl -X POST http://localhost:4000/claude-code/plugins \\
-H "Authorization: Bearer sk-..." \\
-H "Content-Type: application/json" \\
-d '{
"name": "my-plugin",
"source": {"source": "github", "repo": "org/my-plugin"},
"version": "1.0.0",
"description": "My awesome plugin"
}'
```
"""
from prisma.errors import UniqueViolationError
try:
prisma_client: Final = await _get_prisma_client()
if not re.match(r"^[a-z0-9-]+$", request.name):
raise HTTPException(
status_code=400,
detail={"error": "Plugin name must be kebab-case (lowercase letters, numbers, hyphens)"},
)
_validate_plugin_source(request.source)
existing: Final[_PluginRecord | None] = await ClaudeCodePluginRepository(prisma_client).table.find_unique(
where={"name": request.name}
)
if existing:
raise _name_conflict_error(request.name)
manifest: Final[Mapping[str, object]] = _build_plugin_manifest(request.name, request)
try:
plugin: Final[_PluginRecord] = await ClaudeCodePluginRepository(prisma_client).table.create(
data={
"name": request.name,View on GitHub (pinned to 77b7c6c40c)
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}.
Example fix
# before
curl -X POST http://localhost:4000/claude-code/plugins \
-d '{"name": "Data_Cleaner", "source": {"source": "github", "repo": "org/dc"}}'
# after
curl -X POST http://localhost:4000/claude-code/plugins \
-d '{"name": "data-cleaner", "source": {"source": "github", "repo": "org/dc"}}' Defensive patterns
Strategy: validation
Validate before calling
import re
def slugify(name: str) -> str:
s = re.sub(r"[^a-zA-Z0-9]+", "-", name.strip().lower()).strip("-")
if not re.fullmatch(r"[a-z0-9-]+", s):
raise ValueError(f"cannot slugify {name!r} to kebab-case")
return s
payload["name"] = slugify(payload["name"]) # 'Data_Cleaner' -> 'data-cleaner' Type guard
const KEBAB = /^[a-z0-9-]+$/;
function isKebabName(n: unknown): n is string {
return typeof n === 'string' && n.length > 0 && KEBAB.test(n);
} Prevention
- 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.
When it happens
Trigger: 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-].
Common situations: 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.
Related errors
- GitHub source must include 'repo' field (e.g., 'org/repo')
- URL source must include 'url' field (e.g., 'https://github.c
- git-subdir source must include 'url' field (e.g., 'https://g
- git-subdir source must include 'path' field (e.g., 'plugins/
- source.source must be 'github', 'url', or 'git-subdir'
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/b353934e72789425.
Report an issue: GitHub.