github/spec-kit · error · BundlerError

Malformed catalog config at

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.

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.

Appendix: 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)