github/spec-kit · error · ValidationError
Invalid extension.effect '{ext.get('effect')}': must be one
Error message
Invalid extension.effect '{ext.get('effect')}': must be one of {sorted(VALID_EFFECTS)} What it means
The optional extension.effect field must be one of the declared VALID_EFFECTS: 'read-only' or 'read-write'. It declares what filesystem/filesystem-adjacent impact the extension has, and anything outside the allowlist is rejected.
Source
Thrown at src/specify_cli/extensions/__init__.py:338
)
# Validate semantic version
try:
pkg_version.Version(ext["version"])
except pkg_version.InvalidVersion:
raise ValidationError(f"Invalid version: {ext['version']}")
# Validate optional category field (free-form string)
if "category" in ext:
if not isinstance(ext["category"], str) or not ext["category"].strip():
raise ValidationError(
"Invalid extension.category: must be a non-empty string"
)
# Validate optional effect field
if "effect" in ext:
if not isinstance(ext["effect"], str) or ext["effect"] not in VALID_EFFECTS:
raise ValidationError(
f"Invalid extension.effect '{ext.get('effect')}': "
f"must be one of {sorted(VALID_EFFECTS)}"
)
# Validate requires section
requires = self.data["requires"]
if not isinstance(requires, dict):
raise ValidationError(
f"Invalid requires: expected a mapping, got {type(requires).__name__}"
)
if "speckit_version" not in requires:
raise ValidationError("Missing requires.speckit_version")
# Presence alone is not enough: check_compatibility() feeds this value to
# ``SpecifierSet(required)``, guarded only by ``except InvalidSpecifier``,
# which a non-string escapes two different ways. A float/int/bool/None
# raises TypeError from the constructor, while a list or dict is an
# *iterable*, so SpecifierSet accepts it and the failure surfaces much
# later as ``AttributeError: 'str' object has no attribute 'filter'`` fromView on GitHub (pinned to bf88c9f9a8)
Solutions
- Use exactly `effect: read-only` or `effect: read-write`.
- Or omit the effect key if you don't need to declare it.
- Check the error message — it prints sorted(VALID_EFFECTS) so you can copy the exact accepted spelling.
Example fix
# before effect: readonly # after effect: read-only
Defensive patterns
Strategy: type-guard
Validate before calling
VALID_EFFECTS = {"read-only", "read-write"}
def effect_ok(data: dict) -> bool:
eff = data.get("extension", {}).get("effect")
return eff is None or eff in VALID_EFFECTS Type guard
def is_valid_effect(v: object) -> bool:
return v is None or (isinstance(v, str) and v in {"read-only", "read-write"}) Prevention
- Copy the exact literal from the error message (it prints the allowlist).
- Remember only two levels exist: read-only and read-write.
When it happens
Trigger: A manifest sets `effect: readonly` (missing hyphen), `effect: write`, or `effect: 'Read-Write'`. Membership test against frozenset({'read-only','read-write'}) fails and the error lists the valid values.
Common situations: Typing 'readonly' as one word, capitalizing the value, or inventing a third effect like 'destructive' because the author didn't know only two levels exist.
Related errors
- Invalid extension ID '{ext['id']}': must be lowercase alphan
- Invalid version: {ext['version']}
- Invalid extension.category: must be a non-empty string
- Invalid requires: expected a mapping, got {type(requires).__
- Missing requires.speckit_version
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/3bf5d4dd492fa057.
Report an issue: GitHub.