github/spec-kit · error · ValidationError

Invalid {singular} 'file' {label}: {reason}

Error message

Invalid {singular} 'file' {label}: {reason}

What it means

The 'file' value of a provides.templates/scripts entry failed the shared path-safety policy in relative_extension_path_violation() (src/specify_cli/_utils.py:21) — the identical policy applied to command files, keeping manifest validation and the runtime registrar guard from drifting. It rejects non-string/empty values, whitespace padding, backslashes, absolute/anchored paths (POSIX, Windows drive/UNC), '..' traversal, trailing directory slashes, and platform-reserved components. The message label is repr(file) for strings, otherwise the artifact name.

Source

Thrown at src/specify_cli/extensions/__init__.py:615

                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"
                )

            if "strategy" in entry:
                raise ValidationError(
                    f"Invalid {singular} entry '{name}': 'strategy' is not authorable for "
                    "extension-provided artifacts, which always use 'replace' semantics"
                )

            if section == "scripts" and "runtimes" in entry:
                runtimes = entry["runtimes"]
                if not isinstance(runtimes, list) or not all(
                    isinstance(r, str) for r in runtimes
                ):
                    raise ValidationError(

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Use a relative, forward-slash path inside the extension directory: "templates/plan.md".
  2. Remove '..', leading '/', drive letters, and trailing slashes; trim whitespace.
  3. Ship any shared file inside the extension directory instead of pointing outside it.

Example fix

// before
{ "name": "plan", "file": "../shared/templates/plan.md" }
// after
{ "name": "plan", "file": "templates/plan.md" }
Defensive patterns

Strategy: validation

Validate before calling

from specify_cli._utils import relative_extension_path_violation

for section in ("templates", "scripts"):
    for e in manifest.get("provides", {}).get(section, []):
        if isinstance(e, dict):
            reason = relative_extension_path_violation(e.get("file"))
            assert reason is None, f"unsafe {section} file {e.get('file')!r}: {reason}"

Type guard

def is_safe_artifact_file(entry: dict) -> bool:
    from specify_cli._utils import relative_extension_path_violation
    return relative_extension_path_violation(entry.get("file")) is None

Try / catch

try:
    ExtensionManifest.load(path)
except ValidationError as e:
    if "'file'" in str(e) and "must be" in str(e):
        # rewrite the path relative to the extension dir with forward slashes
        ...

Prevention

When it happens

Trigger: {"file": "/opt/ext/t.md"}, {"file": "../shared/t.md"}, {"file": "templates\\t.md"}, {"file": "templates/"}, or a non-string file value. Raised by the artifact validator right after the duplicate-name check.

Common situations: Absolute paths pasted from a dev machine; Windows separators; referencing a shared directory outside the extension with '../'; template generators emitting directory paths.

Related errors


AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14). Data as JSON: /api/errors/4f798833ab7614c7. Report an issue: GitHub.