bmad-code-org/BMAD-METHOD · error · ConfigError

TOML layer is not a file: {path}

Error message

TOML layer is not a file: {path}

What it means

`load_toml` raises this when the path exists and passes `path.exists()` but `path.is_file()` is False — i.e. the path resolves to a directory, a broken symlink, or a non-regular file (socket/device/FIFO). It distinguishes 'present but not a file' from 'absent', because absence may be legitimate for optional layers but a directory masquerading as a config file is always a setup mistake.

Source

Thrown at src/scripts/config_utils.py:24

from pathlib import Path
from typing import Any, Iterable


class ConfigError(ValueError):
    """Raised when a present configuration layer cannot be used safely."""


_KEYED_MERGE_FIELDS = ("code", "id")


def load_toml(path: Path, *, required: bool = False) -> dict[str, Any]:
    """Load a TOML table, allowing absence only for optional layers."""
    if not path.exists():
        if required:
            raise ConfigError(f"required TOML file not found: {path}")
        return {}
    if not path.is_file():
        raise ConfigError(f"TOML layer is not a file: {path}")
    try:
        with path.open("rb") as stream:
            parsed = tomllib.load(stream)
    except tomllib.TOMLDecodeError as error:
        raise ConfigError(f"failed to parse {path}: {error}") from error
    except OSError as error:
        raise ConfigError(f"failed to read {path}: {error}") from error
    if not isinstance(parsed, dict):
        raise ConfigError(f"TOML layer did not parse to a table: {path}")
    return parsed


def _detect_keyed_merge_field(items: list[Any]) -> str | None:
    if not items or not all(isinstance(item, dict) for item in items):
        return None
    for candidate in _KEYED_MERGE_FIELDS:
        if all(candidate in item for item in items):
            for item in items:

View on GitHub (pinned to b70486b9bd)

Solutions

  1. Inspect the path: `ls -la <path>` and confirm it is a directory or broken symlink.
  2. Remove or rename the directory and recreate the file: `rm -rf <path> && <create the TOML file>`.
  3. If it is a symlink, repoint it at the real TOML file with `ln -sf <target> <path>`.
  4. Audit any tool that writes into the `_bmad` tree to ensure it writes files, not folders, at config paths.

Example fix

# before: _bmad/config.toml is a directory
rm -rf _bmad/config.toml
# after: recreate as a file
printf '[project]\nname = "x"\n' > _bmad/config.toml
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path
p = Path(path)
assert not p.exists() or p.is_file(), f'TOML layer is not a file: {p}'

Type guard

def is_regular_file(path) -> bool:
    p = Path(path)
    return p.exists() and p.is_file()

Try / catch

try:
    load_toml(path, required=required)
except ConfigError as e:
    print(f"error: {e}", file=sys.stderr); sys.exit(2)

Prevention

When it happens

Trigger: Someone `mkdir _bmad/config.toml` instead of creating the file; a symlink pointing at a directory; a build step that materialised a config name as a folder (e.g. per-skill output dir colliding with a config filename).

Common situations: A misconfigured generator that writes config 'files' as directories; symlink drift after a repo move; an accidental `git mv` that turned a file into a folder.

Related errors


AI-assisted analysis of bmad-code-org/BMAD-METHOD@b70486b9bd (2026-08-13). Data as JSON: /api/errors/d56907a2d2dc6357. Report an issue: GitHub.