github/spec-kit · error · BundlerError

Malformed catalog config at {config_path}: expected a mappin

Error message

Malformed catalog config at {config_path}: expected a mapping at the top level, got {type(data).__name__}.

What it means

Raised by the bundle-catalog config reader when a bundle-catalogs.yml file parses to something other than a YAML mapping at the top level (e.g. the file contains a list, scalar, or is empty-but-typed like '[]' or 'false'). The bundler merges catalog sources per scope (user/project), and a non-mapping document cannot be merged, so it fails fast with the offending path and detected type name.

Source

Thrown at src/specify_cli/bundler/models/catalog.py:278

    project_config = Path(project_root) / ".specify" / CONFIG_FILENAME
    if project_config.exists():
        ensure_within(project_root, project_config)
    _merge_config(by_id, project_config, Scope.PROJECT)

    return sorted(by_id.values(), key=lambda s: (s.priority, s.id))


def _merge_config(by_id: dict[str, CatalogSource], config_path: Path, scope: Scope) -> None:
    if not config_path.exists():
        return
    # ``load_yaml`` returns ``{}`` only for an empty document and the raw parse
    # otherwise, so a non-mapping top level (a YAML list or scalar, including
    # the falsy ``[]``/``false``/``0``/``''``) is caught here and raised —
    # matching the sibling reader commands_impl/catalog_config._read. #3623
    # aligned the inner non-list ``catalogs`` value between the two readers.
    data = load_yaml(config_path)
    if not isinstance(data, dict):
        raise BundlerError(
            f"Malformed catalog config at {config_path}: expected a mapping at "
            f"the top level, got {type(data).__name__}."
        )
    # Reject an unsupported major schema version, matching the sibling reader
    # commands_impl/catalog_config._read. Without this, a file written by a
    # newer/incompatible Spec Kit was silently parsed under v1 assumptions on
    # the resolution path (bundle search/install), while the other reader
    # rejected it — the two readers disagreed. An absent schema_version stays
    # valid (backward compatible with configs that omit it).
    schema_version = data.get("schema_version")
    if schema_version is not None and (
        str(schema_version).strip().split(".")[0]
        != CONFIG_SCHEMA_VERSION.split(".")[0]
    ):
        raise BundlerError(
            f"Unsupported catalog config schema version "
            f"'{str(schema_version).strip()}' at {config_path}; this Spec Kit "
            f"understands version {CONFIG_SCHEMA_VERSION}. The file may have been "

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Open the file named in the error and make the top level a YAML mapping, e.g. 'schema_version: 1' / 'catalogs:' keys at column 0.
  2. Remove any leading '-' sequence entries or stray scalars at the top level.
  3. Validate the file with a YAML linter or 'python -c "import yaml,sys; d=yaml.safe_load(open(sys.argv[1])); print(type(d))"' expecting dict.
  4. If the file is unwanted, delete it — _merge_config returns early when the path does not exist.

Example fix

# before (bundle-catalogs.yml)
- id: my-catalog
  url: https://example.com/catalog.yml

# after
schema_version: 1
catalogs:
  - id: my-catalog
    url: https://example.com/catalog.yml
Defensive patterns

Strategy: validation

Validate before calling

import yaml
from pathlib import Path

def catalog_config_is_valid(path: Path) -> bool:
    if not path.exists():
        return True  # absent config is fine
    data = yaml.safe_load(path.read_text())
    return data is None or isinstance(data, dict)

Type guard

def is_catalog_mapping(data: object) -> bool:
    return data is None or isinstance(data, dict)

Try / catch

from specify_cli.bundler.models.catalog import BundlerError
try:
    resolve_catalogs(...)
except BundlerError as e:
    if "Malformed catalog config" in str(e):
        # show path + type from message, prompt user to fix or delete file
        ...

Prevention

When it happens

Trigger: Calling the catalog resolution path (bundle search/install) while a bundle-catalogs.yml exists whose top-level node is a sequence (starts with '- '), a bare scalar, or a falsy value like '[]', 'false', '0', or an empty quoted string. _merge_config(by_id, config_path, scope) hits the isinstance(data, dict) check after load_yaml.

Common situations: Hand-editing bundle-catalogs.yml and accidentally putting the 'catalogs:' key under a list item; pasting a YAML fragment that starts with '- '; truncating the file so only a scalar remains; converting the file from another format and losing the top-level mapping.

Understand the failure class

Related errors


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