github/spec-kit · error · BundlerError
Malformed catalog config at {path}: each catalog entry must
Error message
Malformed catalog config at {path}: each catalog entry must be a mapping, got {type(entry).__name__}. What it means
Raised during the per-entry loop over `catalogs` when an individual entry is not a mapping. Each catalog source must be a mapping with at least `id`, `url`, `priority`, `install_policy`; a bare string or nested list inside `catalogs` is rejected here.
Source
Thrown at src/specify_cli/bundler/commands_impl/catalog_config.py:74
!= CONFIG_SCHEMA_VERSION.split(".")[0]
):
raise BundlerError(
f"Unsupported catalog config schema version "
f"'{str(schema_version).strip()}' at {path}; this Spec Kit "
f"understands version {CONFIG_SCHEMA_VERSION}. The file may have been "
"written by a newer version or is corrupt."
)
catalogs = data.get("catalogs")
if catalogs is None:
return []
if not isinstance(catalogs, list):
raise BundlerError(
f"Malformed catalog config at {path}: 'catalogs' must be a list, "
f"got {type(catalogs).__name__}."
)
for entry in catalogs:
if not isinstance(entry, dict):
raise BundlerError(
f"Malformed catalog config at {path}: each catalog entry must be "
f"a mapping, got {type(entry).__name__}."
)
return list(catalogs)
def _write(project_root: Path, catalogs: list[dict]) -> None:
payload = {"schema_version": CONFIG_SCHEMA_VERSION, "catalogs": catalogs}
dump_yaml(_config_path(project_root), payload, within=project_root)
def _slug(value: str) -> str:
# Lowercase so derived ids are deterministic and case-insensitive across
# platforms (e.g. 'Team-A.json' and 'team-a.json' yield the same id),
# keeping the case-sensitive duplicate check from admitting logical dupes.
return "".join(ch if ch.isalnum() else "-" for ch in value.lower()).strip("-")
View on GitHub (pinned to bf88c9f9a8)
Solutions
- Convert each entry to a mapping with `id`, `url`, `priority`, and `install_policy` keys
- Remove stray scalar entries
- Let `add_source` generate well-formed entries for you
Example fix
# before
catalogs:
- https://example.com/c.json
# after
catalogs:
- id: example
url: https://example.com/c.json
priority: 50
install_policy: auto Defensive patterns
Strategy: validation
Validate before calling
for i, entry in enumerate(data.get("catalogs") or []):
if not isinstance(entry, dict):
raise SystemExit(f"catalogs[{i}] must be a mapping with id/url/priority/install_policy") Type guard
def all_entries_are_mappings(data: dict) -> bool:
return all(isinstance(e, dict) for e in (data.get("catalogs") or [])) Try / catch
try:
read_catalog_config(project_root)
except BundlerError as exc:
if "each catalog entry must be a mapping" in str(exc):
# replace scalar entries with {id, url, priority, install_policy} objects
raise
raise Prevention
- Generate entries with `specify bundle catalog add` rather than writing shorthands by hand
- Validate each entry is a dict before merging configs in scripts
When it happens
Trigger: `catalogs: ["community", "https://..."]` — a list of strings instead of a list of objects; or `- - id: x` producing a nested list.
Common situations: Shorthand attempt to list just URLs; converting from a simple `.gitignore`-style list format; a bad merge that collapsed entries.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Malformed catalog config at {path}: expected a mapping at th
- Malformed catalog config at {path}: 'catalogs' must be a lis
- Malformed catalog config at {config_path}: expected a mappin
- Malformed catalog config at {config_path}: 'catalogs' must b
- A catalog url is required.
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/8165e76e7242fbb0.
Report an issue: GitHub.