github/spec-kit · error · ValidationError
Invalid {singular} name '{name}': must be lowercase alphanum
Error message
Invalid {singular} name '{name}': must be lowercase alphanumeric with hyphens only What it means
A template/script artifact name did not match ^[a-z0-9-]+$ (VALID_EXTENSION_ARTIFACT_NAME_PATTERN, extensions/__init__.py:67). Unlike commands, these names are not namespaced, but they must be a lowercase alphanumeric slug with hyphens — no uppercase, underscores, dots, or symbols. This mirrors the same constraint applied to extension.id.
Source
Thrown at src/specify_cli/extensions/__init__.py:601
later duplicate would be silently unreachable while still being
exposed by ``ExtensionManifest.templates``/``.scripts``.
"""
seen_names: set[str] = set()
for entry in entries:
if not isinstance(entry, dict):
raise ValidationError(
f"Each entry in 'provides.{section}' must be a mapping"
)
if "name" not in entry or "file" not in entry:
raise ValidationError(f"{singular.capitalize()} missing 'name' or 'file'")
name = entry["name"]
if not isinstance(name, str):
raise ValidationError(
f"Invalid {singular} name: expected a string, got {type(name).__name__}"
)
if not VALID_EXTENSION_ARTIFACT_NAME_PATTERN.match(name):
raise ValidationError(
f"Invalid {singular} name '{name}': "
"must be lowercase alphanumeric with hyphens only"
)
if name in seen_names:
raise ValidationError(
f"Duplicate {singular} name '{name}' in 'provides.{section}'"
)
seen_names.add(name)
file_value = entry["file"]
reason = relative_extension_path_violation(file_value)
if reason:
label = repr(file_value) if isinstance(file_value, str) else f"for {singular} '{name}'"
raise ValidationError(f"Invalid {singular} 'file' {label}: {reason}")
if "description" in entry and not isinstance(entry["description"], str):
raise ValidationError(
f"Invalid {singular} description for '{name}': expected a string"View on GitHub (pinned to bf88c9f9a8)
Solutions
- Rename to a lowercase hyphen slug: "plan-template" instead of "Plan_Template" or "plan.md".
- Strip dots and slashes; the name is an identifier, not a path (the path belongs in 'file').
- Keep 'file' as-is — only the name needs the slug form.
Example fix
// before
{ "name": "Plan_Template", "file": "templates/plan.md" }
// after
{ "name": "plan-template", "file": "templates/plan.md" } Defensive patterns
Strategy: validation
Validate before calling
import re
SLUG = re.compile(r"^[a-z0-9-]+$")
for section in ("templates", "scripts"):
for entry in manifest.get("provides", {}).get(section, []):
if isinstance(entry, dict) and isinstance(entry.get("name"), str):
assert SLUG.match(entry["name"]), f"non-slug name {entry['name']!r}" Type guard
def is_artifact_slug(name: str) -> bool:
import re
return bool(re.match(r"^[a-z0-9-]+$", name)) Try / catch
try:
ExtensionManifest.load(path)
except ValidationError as e:
if "lowercase alphanumeric with hyphens" in str(e):
# lowercase the name, replace _ and . with -, and retry
... Prevention
- Use lowercase-hyphen slugs for artifact names.
- Do not include file extensions ('.md') or dots in the name.
- Run a slug lint on generated manifests.
When it happens
Trigger: "name": "Plan", "name": "plan_template", "name": "plan.md", or "name": "plan/v1". Raised by the regex check right after the string-type check in the artifact validator.
Common situations: Reusing a file basename with an extension (plan.md) as the name; PascalCase names from code constants; underscores carried over from Python identifiers.
Related errors
- Invalid command name '{cmd['name']}': must follow pattern 's
- Extension ID '{manifest.id}' conflicts with core command nam
- Manifest must be a YAML mapping, got {type(data).__name__}:
- Missing required field: {field}
- Invalid extension: expected a mapping, got {type(ext).__name
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/0a38b5423328f2b9.
Report an issue: GitHub.