{"record":{"id":"d48714380687fca7","repo":"mvanhorn/last30days-skill","slug":"brave-api-key-is-required-when-web-backend-brave","errorCode":null,"errorMessage":"BRAVE_API_KEY is required when web_backend='brave'","messagePattern":"BRAVE_API_KEY is required when web_backend='brave'","errorType":"validation","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"skills/last30days/scripts/lib/grounding.py","lineNumber":254,"sourceCode":"        elif config.get(\"EXA_API_KEY\"):\n            backend = \"exa\"\n        elif config.get(\"SERPER_API_KEY\"):\n            backend = \"serper\"\n        elif config.get(\"PARALLEL_API_KEY\"):\n            backend = \"parallel\"\n        elif env.keyless_web_allowed(config):\n            # No paid key and the host has no native search -> use the keyless\n            # floor. On a native-search host this branch is skipped (the model\n            # supplies web results itself), so the engine returns nothing here.\n            backend = \"keyless\"\n        else:\n            return [], {}\n    items: list[dict] = []\n    artifact: dict = {}\n    if backend == \"brave\":\n        key = config.get(\"BRAVE_API_KEY\")\n        if not key:\n            raise RuntimeError(\"BRAVE_API_KEY is required when web_backend='brave'\")\n        items, artifact = brave_search(query, date_range, key)\n    elif backend == \"exa\":\n        key = config.get(\"EXA_API_KEY\")\n        if not key:\n            raise RuntimeError(\"EXA_API_KEY is required when web_backend='exa'\")\n        items, artifact = exa_search(query, date_range, key)\n    elif backend == \"serper\":\n        key = config.get(\"SERPER_API_KEY\")\n        if not key:\n            raise RuntimeError(\"SERPER_API_KEY is required when web_backend='serper'\")\n        items, artifact = serper_search(query, date_range, key)\n    elif backend == \"parallel\":\n        key = config.get(\"PARALLEL_API_KEY\")\n        if not key:\n            raise RuntimeError(\"PARALLEL_API_KEY is required when web_backend='parallel'\")\n        items, artifact = parallel_search(query, date_range, key)\n    elif backend == \"keyless\":\n        items, artifact = web_search_keyless.keyless_search(query, date_range, config)","sourceCodeStart":236,"sourceCodeEnd":272,"githubUrl":"https://github.com/mvanhorn/last30days-skill/blob/c7460f6114449ddfe6ea3fc2f23c3d910c0e740c/skills/last30days/scripts/lib/grounding.py#L236-L272","documentation":"Raised by the web grounding dispatcher in grounding.py when the resolved web backend is 'brave' but the config dict has no truthy BRAVE_API_KEY. The backend can be forced via web_backend='brave' (config or --web-backend flag), which is an explicit instruction that overrides the automatic keyless/paid-key resolution — so when the matching key is absent the engine fails loudly rather than silently returning zero web results.","triggerScenarios":"Setting web_backend='brave' in config/env/CLI while BRAVE_API_KEY is unset, empty, or whitespace; the key exists in a different .env than the one loaded; typo in the variable name (BRAVE_KEY, BRAVEAPI_KEY).","commonSituations":"Config is copied between machines without the secret; the key is defined in the shell but the engine runs from a harness (Claude Code, cron) that does not inherit it; LAST30DAYS .env file lives in a non-default location.","solutions":["Set BRAVE_API_KEY in the environment or the last30days .env file (see env.py auth patterns) and re-run.","If you did not intend to force Brave, remove the web_backend='brave' setting so the engine falls back to automatic backend resolution (paid keys present, else keyless floor).","Verify with: python -c \"from last30days.scripts.lib.env import load_config; print(bool(load_config().get('BRAVE_API_KEY')))\" or an equivalent config dump that masks values."],"exampleFix":"# before\nexport WEB_BACKEND=brave   # no BRAVE_API_KEY anywhere\n\n# after\necho 'BRAVE_API_KEY=<your-key>' >> ~/.config/last30days/.env\nexport WEB_BACKEND=brave","handlingStrategy":"validation","validationCode":"from skills.last30days.scripts.lib.env import load_config  # project env loader\n\nconfig = load_config()\nif config.get(\"web_backend\") == \"brave\":\n    assert config.get(\"BRAVE_API_KEY\"), \"BRAVE_API_KEY must be set for brave backend\"","typeGuard":null,"tryCatchPattern":"try:\n    grounding.web_search_with_backend(query, config)\nexcept RuntimeError as e:\n    if 'BRAVE_API_KEY' in str(e):\n        # config error, not transient: fix env or unset web_backend\n        ...","preventionTips":["Only force web_backend='brave' on machines whose .env carries BRAVE_API_KEY.","Prefer leaving web_backend unset so automatic resolution never picks a keyless paid backend.","Add a startup config assertion for the backend/key pair in scripts that pin a backend."],"tags":["config","api-key","web-search","brave"],"backgroundTag":null,"analyzedSha":"c7460f6114449ddfe6ea3fc2f23c3d910c0e740c","analyzedAt":"2026-08-15T03:34:49.540Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}