github/spec-kit · error · ValidationError

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

Error message

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

What it means

The 'file' field of a provides.commands entry failed the shared path-safety policy in relative_extension_path_violation() (src/specify_cli/_utils.py:21). That single policy is reused by both manifest-load validation and the runtime CommandRegistrar guard so the two cannot drift. It rejects non-string/empty values, leading/trailing whitespace, backslash separators, absolute/anchored paths (POSIX /abs, Windows C:\ or C:foo, UNC), '..' traversal, directory-suffixed values, and platform-reserved/invalid components. The label in the message is the repr of the offending string, or the command name when the value is not even a string.

Source

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

            # relative_extension_path_violation() below already rejects a
            # non-string value.
            if not isinstance(cmd["name"], str):
                raise ValidationError(
                    f"Invalid command name: expected a string, "
                    f"got {type(cmd['name']).__name__}"
                )

            # Validate the 'file' field at manifest-load time using the single
            # shared policy in relative_extension_path_violation(), so manifest
            # validation cannot drift from the runtime registrar guard. This is
            # defense-in-depth: the command/skill/preset readers also contain
            # the resolved path, but rejecting an unsafe value here surfaces a
            # clear error instead of silently skipping the command.
            cmd_file = cmd["file"]
            reason = relative_extension_path_violation(cmd_file)
            if reason:
                label = repr(cmd_file) if isinstance(cmd_file, str) else f"for command '{cmd.get('name')}'"
                raise ValidationError(f"Invalid command 'file' {label}: {reason}")

            # Validate command name format
            if not EXTENSION_COMMAND_NAME_PATTERN.match(cmd["name"]):
                corrected = self._try_correct_command_name(cmd["name"], ext["id"])
                if corrected:
                    self.warnings.append(
                        f"Command name '{cmd['name']}' does not follow the required pattern "
                        f"'speckit.{{extension}}.{{command}}'. Registering as '{corrected}'. "
                        f"The extension author should update the manifest to use this name."
                    )
                    rename_map[cmd["name"]] = corrected
                    cmd["name"] = corrected
                else:
                    raise ValidationError(
                        f"Invalid command name '{cmd['name']}': "
                        "must follow pattern 'speckit.{extension}.{command}'"
                    )

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Change the file value to a relative, forward-slash path inside the extension directory, e.g. "commands/build.md".
  2. Remove leading/trailing whitespace and any '..' segments, drive letters, or leading slashes.
  3. Ensure the value is a non-empty string (not null/int) and names a file, not a directory (no trailing '/').

Example fix

// before
{ "name": "speckit.myext.build", "file": "/home/me/ext/commands/build.md" }
// after
{ "name": "speckit.myext.build", "file": "commands/build.md" }
Defensive patterns

Strategy: validation

Validate before calling

from specify_cli._utils import relative_extension_path_violation

for cmd in manifest.get("provides", {}).get("commands", []):
    reason = relative_extension_path_violation(cmd.get("file"))
    if reason:
        raise SystemExit(f"bad file for {cmd.get('name')}: {reason}")

Type guard

def is_safe_relative_file(value: object) -> bool:
    from specify_cli._utils import relative_extension_path_violation
    return relative_extension_path_violation(value) is None

Try / catch

try:
    ExtensionManifest.load(path)
except ValidationError as e:
    if "Invalid command 'file'" in str(e):
        # rewrite file to a relative forward-slash path inside the extension dir
        ...

Prevention

When it happens

Trigger: A command entry with "file": "/etc/commands/x.md", "file": "..\\..\\x.md", "file": "commands/" (directory suffix), "file": " commands/x.md " (whitespace), or a non-string file value such as null or 3. Any of these hit ExtensionManifest._validate() during extension install/load.

Common situations: Authors copying absolute paths from local dev machines into the manifest; Windows authors using backslashes; manifests written by hand with trailing slashes; thinking 'file' can point outside the extension directory. Because the policy is defense-in-depth, even values the registrar might have skipped now fail loudly at load time.

Related errors


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