{"record":{"id":"10d31febe48f99ca","repo":"github/spec-kit","slug":"providers-i-hosts-must-be-a-non-empty-array","errorCode":null,"errorMessage":"providers[{i}]: 'hosts' must be a non-empty array","messagePattern":"providers\\[(.+?)\\]: 'hosts' must be a non-empty array","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/authentication/config.py","lineNumber":131,"sourceCode":"            pass  # stat failed — skip permission check\n\n    raw = json.loads(config_path.read_text(encoding=\"utf-8\"))\n\n    if not isinstance(raw, dict):\n        raise ValueError(f\"auth.json must be a JSON object, got {type(raw).__name__}\")\n\n    providers_raw = raw.get(\"providers\")\n    if not isinstance(providers_raw, list):\n        raise ValueError(\"auth.json must contain a 'providers' array\")\n\n    entries: list[AuthConfigEntry] = []\n    for i, entry_raw in enumerate(providers_raw):\n        if not isinstance(entry_raw, dict):\n            raise ValueError(f\"providers[{i}]: must be a JSON object\")\n\n        hosts = entry_raw.get(\"hosts\")\n        if not isinstance(hosts, list) or not hosts:\n            raise ValueError(f\"providers[{i}]: 'hosts' must be a non-empty array\")\n        if not all(isinstance(h, str) and h.strip() for h in hosts):\n            raise ValueError(f\"providers[{i}]: each host must be a non-empty string\")\n        # Normalize hosts: strip whitespace and lowercase\n        hosts = [h.strip().lower() for h in hosts]\n        # Reject dangerous wildcard forms (e.g. *github.com matches github.com.evil.com)\n        for h in hosts:\n            if not _is_valid_host_pattern(h):\n                raise ValueError(\n                    f\"providers[{i}]: invalid host pattern {h!r}. \"\n                    \"Only exact hostnames or '*.suffix' forms are allowed \"\n                    \"(e.g. 'github.com' or '*.visualstudio.com').\"\n                )\n\n        provider = entry_raw.get(\"provider\", \"\")\n        if not isinstance(provider, str) or not provider:\n            raise ValueError(f\"providers[{i}]: 'provider' must be a non-empty string\")\n\n        auth = entry_raw.get(\"auth\", \"\")","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/authentication/config.py#L113-L149","documentation":"Inside each auth.json provider entry, the 'hosts' key must be a non-empty JSON array. A missing hosts key, hosts: null, hosts: \"github.com\" (a string), or hosts: [] (empty array) raises ValueError('providers[i]: \\'hosts\\' must be a non-empty array') with the provider's index.","triggerScenarios":"Omitting hosts because a single 'host' singular key was used instead; hosts written as a comma-separated string; an intentionally emptied array left while disabling a provider; hosts: {} from a mapping-style config.","commonSituations":"Copying a provider entry and deleting the hosts to 'fill in later'; singular/plural key confusion ('host' vs 'hosts'); converting configs from tools that accept a single string hostname.","solutions":["Set hosts to a non-empty array of hostnames: \"hosts\": [\"dev.azure.com\"].","Fix the singular typo: 'host' is not read — the key must be 'hosts'.","Remove the whole provider entry if you no longer want that provider, instead of leaving hosts empty."],"exampleFix":"// before\n{ \"providers\": [ {\"host\": \"github.com\", \"provider\": \"github\"} ] }\n// after\n{ \"providers\": [ {\"hosts\": [\"github.com\"], \"provider\": \"github\", \"auth\": \"bearer\"} ] }","handlingStrategy":"validation","validationCode":"raw = json.loads(Path(\"auth.json\").read_text(encoding=\"utf-8\"))\nbad = [\n    i for i, e in enumerate(raw.get(\"providers\", []))\n    if not isinstance(e.get(\"hosts\"), list) or not e.get(\"hosts\")\n]\nif bad:\n    raise SystemExit(f\"providers entries need non-empty hosts arrays; bad indices: {bad}\")","typeGuard":"from typing import Any\n\ndef has_nonempty_hosts(entry: Any) -> bool:\n    \"\"\"True when entry is an object whose 'hosts' is a non-empty list.\"\"\"\n    return (\n        isinstance(entry, dict)\n        and isinstance(entry.get(\"hosts\"), list)\n        and len(entry[\"hosts\"]) > 0\n    )","tryCatchPattern":"try:\n    entries = load_auth_config(path)\nexcept ValueError as exc:\n    if \"'hosts' must be a non-empty array\" in str(exc):\n        raise SystemExit(f\"fix hosts for provider: {exc}\") from exc\n    raise","preventionTips":["Always use the plural key 'hosts' with an array value.","Delete unused provider entries instead of emptying their hosts.","Accept only exact hostnames or '*.suffix' forms — other patterns fail the next check."],"tags":["authentication","json","configuration","schema"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}