github/spec-kit · error · BundlerError
Catalog source '{resolved_id}' (or url) already exists in th
Error message
Catalog source '{resolved_id}' (or url) already exists in this project. What it means
Raised by add_source() when registering a catalog source whose resolved id or canonical url collides with an entry already present in the project's catalog config. The function canonicalizes the url (e.g. expands relative local paths to absolute file paths) and derives an id from it when none is supplied, so the duplicate check covers both explicit and derived collisions. This protects the project catalog list from ambiguous, shadowing entries.
Source
Thrown at src/specify_cli/bundler/commands_impl/catalog_config.py:191
# or "https://user@". Validating here keeps junk out of
# bundle-catalogs.yml instead of failing later at fetch time.
is_localhost = hostname in ("localhost", "127.0.0.1", "::1")
if parsed.scheme.lower() != "https" and not is_localhost:
raise BundlerError(
f"Catalog url must use HTTPS (got {parsed.scheme}://). "
"HTTP is only allowed for localhost."
)
if not hostname:
raise BundlerError(f"Catalog url must be a valid URL with a host: {url}")
url = _canonicalize_url(url)
install_policy = InstallPolicy.parse(policy)
resolved_id = (source_id or _derive_id(url)).strip()
catalogs = _read(project_root)
for existing in catalogs:
if existing.get("id") == resolved_id or existing.get("url") == url:
raise BundlerError(
f"Catalog source '{resolved_id}' (or url) already exists in this project."
)
entry = {
"id": resolved_id,
"url": url,
"priority": int(priority),
"install_policy": install_policy.value,
}
catalogs.append(entry)
_write(project_root, catalogs)
return CatalogSource.from_dict(entry, Scope.PROJECT)
def remove_source(project_root: Path, id_or_url: str) -> str:
target = id_or_url.strip()
if target in _BUILTIN_IDS:
raise BundlerError(View on GitHub (pinned to bf88c9f9a8)
Solutions
- Remove the existing source first: remove_source(project_root, '<existing-id-or-url>'), then re-add it with the new settings.
- Check the current project catalog (via the list/read API or the catalog config file) before calling add_source, and skip when the id or canonical url is already present.
- If the collision is only on url but you want a distinct id, pass an explicit source_id and a different url (or vice versa) only if a genuinely different source is intended.
- Make idempotent provisioning scripts tolerant: catch this BundlerError and treat 'already exists' as success.
Example fix
# before
add_source(root, "community", "https://example.com/catalog.json") # second run raises
# after
existing = {c.id: c for c in read_sources(root)}
if "community" not in existing:
add_source(root, "community", "https://example.com/catalog.json") Defensive patterns
Strategy: validation
Validate before calling
from specify_cli.bundler.commands_impl import catalog_config
def add_source_safe(root, source_id, url, priority, policy):
existing = catalog_config._read(root)
for c in existing:
if c.get("id") == source_id or c.get("url") == url:
return None # already present
return catalog_config.add_source(root, source_id, url, priority, policy) Try / catch
try:
add_source(root, sid, url, priority=10, policy="install-allowed")
except BundlerError as e:
if "already exists" in str(e):
pass # idempotent provisioning: treat as success
else:
raise Prevention
- Make provisioning scripts idempotent: check the catalog list before adding.
- Use stable, explicit source_ids so url changes do not create accidental duplicates.
- Wrap add/remove flows in a single helper that catches 'already exists'/'not found' as no-ops.
When it happens
Trigger: Calling add_source(project_root, url=...) twice with the same url; calling add_source with a source_id equal to an existing entry's id; adding a local path that _canonicalize_url() expands to the same absolute url as a previously added source; adding a built-in id that already appears in project scope.
Common situations: Re-running a setup script or 'specify bundler catalog add' command twice; adding a source that was already added in an earlier session; copy-pasting onboarding docs that add the same default catalog; CI provisioning steps that run unconditionally instead of checking current state.
Related errors
- '{target}' is a built-in default source and cannot be delete
- Each catalog source must be a mapping.
- Malformed catalog config at {path}: expected a mapping at th
- Unsupported catalog config schema version '{str(schema_versi
- Malformed catalog config at {path}: 'catalogs' must be a lis
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/8a63a1b269e72711.
Report an issue: GitHub.