{"repo":"NousResearch/hermes-agent","public_id":"33465","display_label":"Cron scheduler missing BSM secret resolution fixes","summary":"Multiple fixes and a bug report addressing the cron scheduler's failure to resolve Bitwarden Secrets Manager secrets, with related hardening around null-safe delivery and environment loading.","subsystem":"cron","counts":{"members":6,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":4},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":56144},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-07-01T08:05:33+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.3333333333333333},"fix_edges":[{"issue_number":32896,"pr_number":56144,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":33465,"pr_number":56144,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":33465,"title":"[Bug]: cron scheduler missing BSM resolution","url":null,"state":"closed"},{"kind":"pr","number":33479,"title":"fix(cron): pull Bitwarden Secrets Manager secrets in run_job","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":33527,"title":"fix(cron): harden ticker thread, null-safe deliver, and BSM env loader","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#56144","number":56144}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":33667,"title":"fix(cron): resolve BSM secrets in scheduler run_job (#33465)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#33465","number":33465},{"kind":"pr","value":"#33479","number":33479},{"kind":"pr","value":"#33527","number":33527}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":34041,"title":"fix(cron): use load_hermes_dotenv so BSM secrets resolve for cron jobs","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"836732f54f7235d8cbae01f5cd4e1b86b0b70b49","number":0},{"kind":"pr","value":"#33465","number":33465},{"kind":"pr","value":"#56144","number":56144}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":56144,"title":"fix(cron): null-safe deliver in cron list + re-resolve BSM secrets per run","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cron/scheduler.py","hermes_cli/cron.py","tests/cron/test_cron_provider_pin.py","tests/cron/test_scheduler.py","tests/hermes_cli/test_cron.py"],"fix_hunks":[{"file":"cron/scheduler.py","hunks":["@@ -2225,12 +2225,23 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:\n \n     try:\n         # Re-read .env and config.yaml fresh every run so provider/key\n-        # changes take effect without a gateway restart.\n-        from dotenv import load_dotenv\n-        try:\n-            load_dotenv(str(_get_hermes_home() / \".env\"), override=True, encoding=\"utf-8\")\n-        except UnicodeDecodeError:\n-            load_dotenv(str(_get_hermes_home() / \".env\"), override=True, encoding=\"latin-1\")\n+        # changes take effect without a gateway restart. Route through\n+        # load_hermes_dotenv (not a bare load_dotenv) and reset the secret-\n+        # source cache first: startup already applied external secrets and\n+        # recorded this HERMES_HOME in _APPLIED_HOMES, so a naive reload would\n+        # re-apply only the .env placeholder and never re-resolve a Bitwarden/\n+        # BSM-backed secret — leaving cron jobs 401'ing on the placeholder\n+        # (#33465). Clearing the cache forces the re-pull; the resolved secret\n+        # overrides the placeholder only when secrets.bitwarden.override_existing\n+        # is set (mirrors startup), and the Bitwarden value-cache keeps the\n+        # forced re-pull off the network. load_hermes_dotenv also handles the\n+        # utf-8/latin-1 encoding fallback internally.\n+        from hermes_cli.env_loader import (\n+            load_hermes_dotenv,\n+            reset_secret_source_cache,\n+        )\n+        reset_secret_source_cache()\n+        load_hermes_dotenv(hermes_home=_get_hermes_home())\n \n         delivery_target = _resolve_delivery_target(job)\n         if delivery_target:"]},{"file":"hermes_cli/cron.py","hunks":["@@ -137,7 +137,11 @@ def cron_list(show_all: bool = False):\n         repeat_completed = repeat_info.get(\"completed\", 0)\n         repeat_str = f\"{repeat_completed}/{repeat_times}\" if repeat_times else \"∞\"\n \n-        deliver = job.get(\"deliver\", [\"local\"])\n+        # `deliver` may be present-but-null in the job record (same pitfall as\n+        # `repeat` above), so coalesce to the default rather than relying on the\n+        # dict-default, which only applies to a missing key. A null value would\n+        # otherwise reach `\", \".join(None)` and crash the whole listing (#32896).\n+        deliver = job.get(\"deliver\") or [\"local\"]\n         if isinstance(deliver, str):\n             deliver = [deliver]\n         deliver_str = \", \".join(deliver)"]},{"file":"tests/cron/test_cron_provider_pin.py","hunks":["@@ -52,7 +52,8 @@ def _run_with_current_provider(job, current_provider, tmp_path):\n     fake_db = MagicMock()\n     with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n          patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-         patch(\"dotenv.load_dotenv\"), \\\n+         patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+         patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n          patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n          patch(\n              \"hermes_cli.runtime_provider.resolve_runtime_provider\",","@@ -252,7 +253,8 @@ def _run_with_current_provider_and_model(job, current_provider, current_model, t\n     with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n          patch(\"cron.scheduler._get_hermes_home\", return_value=tmp_path), \\\n          patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-         patch(\"dotenv.load_dotenv\"), \\\n+         patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+         patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n          patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n          patch(\n              \"hermes_cli.runtime_provider.resolve_runtime_provider\","]},{"file":"tests/cron/test_scheduler.py","hunks":["@@ -966,7 +966,8 @@ def test_run_job_passes_session_db_and_cron_platform(self, tmp_path):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\n                  \"hermes_cli.runtime_provider.resolve_runtime_provider\",","@@ -1012,7 +1013,8 @@ def test_run_job_suppresses_empty_turn_explainer(self, tmp_path):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\n                  \"hermes_cli.runtime_provider.resolve_runtime_provider\",","@@ -1055,7 +1057,8 @@ def test_run_job_real_report_on_empty_reason_still_delivers(self, tmp_path):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\n                  \"hermes_cli.runtime_provider.resolve_runtime_provider\",","@@ -1095,7 +1098,8 @@ def test_run_job_titles_cron_session_from_job_not_important_hint(self, tmp_path)\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\n                  \"hermes_cli.runtime_provider.resolve_runtime_provider\",","@@ -1132,7 +1136,8 @@ def test_run_job_closes_agent_on_failure_to_prevent_fd_leak(self, tmp_path):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\n                  \"hermes_cli.runtime_provider.resolve_runtime_provider\",","@@ -1169,7 +1174,8 @@ def test_run_job_reaps_stale_auxiliary_clients_per_tick(self, tmp_path):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\n                  \"hermes_cli.runtime_provider.resolve_runtime_provider\",","@@ -1197,7 +1203,8 @@ def _make_run_job_patches(self, tmp_path):\n         return fake_db, [\n             patch(\"cron.scheduler._hermes_home\", tmp_path),\n             patch(\"cron.scheduler._resolve_origin\", return_value=None),\n-            patch(\"dotenv.load_dotenv\"),\n+            patch(\"hermes_cli.env_loader.load_hermes_dotenv\"),\n+            patch(\"hermes_cli.env_loader.reset_secret_source_cache\"),\n             patch(\"hermes_state.SessionDB\", return_value=fake_db),\n             patch(\n                 \"hermes_cli.runtime_provider.resolve_runtime_provider\",","@@ -1218,7 +1225,7 @@ def test_run_job_passes_enabled_toolsets_to_agent(self, tmp_path):\n             \"enabled_toolsets\": [\"web\", \"terminal\", \"file\"],\n         }\n         fake_db, patches = self._make_run_job_patches(tmp_path)\n-        with patches[0], patches[1], patches[2], patches[3], patches[4], \\\n+        with patches[0], patches[1], patches[2], patches[3], patches[4], patches[5], \\\n              patch(\"run_agent.AIAgent\") as mock_agent_cls:\n             mock_agent = MagicMock()\n             mock_agent.run_conversation.return_value = {\"final_response\": \"ok\"}","@@ -1252,7 +1259,7 @@ def test_run_job_disabled_toolsets_layer_user_config_on_baseline(self, tmp_path)\n             \"enabled_toolsets\": [\"web\", \"terminal\", \"file\"],\n         }\n         fake_db, patches = self._make_run_job_patches(tmp_path)\n-        with patches[0], patches[1], patches[2], patches[3], patches[4], \\\n+        with patches[0], patches[1], patches[2], patches[3], patches[4], patches[5], \\\n              patch(\"run_agent.AIAgent\") as mock_agent_cls:\n             mock_agent = MagicMock()\n             mock_agent.run_conversation.return_value = {\"final_response\": \"ok\"}","@@ -1279,7 +1286,7 @@ def test_run_job_enabled_toolsets_resolves_from_platform_config_when_not_set(sel\n             \"prompt\": \"hello\",\n         }\n         fake_db, patches = self._make_run_job_patches(tmp_path)\n-        with patches[0], patches[1], patches[2], patches[3], patches[4], \\\n+        with patches[0], patches[1], patches[2], patches[3], patches[4], patches[5], \\\n              patch(\"run_agent.AIAgent\") as mock_agent_cls:\n             mock_agent = MagicMock()\n             mock_agent.run_conversation.return_value = {\"final_response\": \"ok\"}","@@ -1307,7 +1314,7 @@ def test_run_job_per_job_toolsets_win_over_platform_config(self, tmp_path):\n         fake_db, patches = self._make_run_job_patches(tmp_path)\n         # Even if the user has ``hermes tools`` configured to enable web+file\n         # for cron, the per-job override wins.\n-        with patches[0], patches[1], patches[2], patches[3], patches[4], \\\n+        with patches[0], patches[1], patches[2], patches[3], patches[4], patches[5], \\\n              patch(\"run_agent.AIAgent\") as mock_agent_cls, \\\n              patch(\n                  \"hermes_cli.tools_config._get_platform_tools\",","@@ -1336,7 +1343,8 @@ def test_run_job_empty_response_returns_empty_not_placeholder(self, tmp_path):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\n                  \"hermes_cli.runtime_provider.resolve_runtime_provider\",","@@ -1412,7 +1420,8 @@ def test_run_job_treats_agent_failure_flag_as_failure(\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\n                  \"hermes_cli.runtime_provider.resolve_runtime_provider\",","@@ -1451,7 +1460,8 @@ def test_run_job_completed_true_without_failed_flag_succeeds(self, tmp_path):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\n                  \"hermes_cli.runtime_provider.resolve_runtime_provider\",","@@ -1493,7 +1503,8 @@ def test_run_job_delivers_max_iteration_fallback_summary(self, tmp_path):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\n                  \"hermes_cli.runtime_provider.resolve_runtime_provider\",","@@ -1613,6 +1624,53 @@ def run_conversation(self, *args, **kwargs):\n         assert os.getenv(\"HERMES_CRON_AUTO_DELIVER_THREAD_ID\") is None\n         fake_db.close.assert_called_once()\n \n+    def test_run_job_resets_secret_source_cache_before_reload(self, tmp_path, monkeypatch):\n+        \"\"\"Each run must clear the secret-source cache before re-reading the\n+        env, so a long-running gateway re-resolves Bitwarden/BSM-backed secrets\n+        instead of leaving the startup .env placeholder in place (#33465).\n+\n+        A bare ``load_dotenv`` re-load can't do this: startup already recorded\n+        this HERMES_HOME in ``_APPLIED_HOMES``, so the external-secret pull\n+        no-ops and only the placeholder is re-applied. The scheduler must call\n+        ``reset_secret_source_cache()`` (forcing the re-pull) and route through\n+        ``load_hermes_dotenv`` (which then re-applies external secret sources).\n+        \"\"\"\n+        job = {\"id\": \"bsm-job\", \"name\": \"bsm\", \"prompt\": \"hello\"}\n+        fake_db = MagicMock()\n+        call_order = []\n+\n+        def _record_reset():\n+            call_order.append(\"reset\")\n+\n+        def _record_load(*args, **kwargs):\n+            call_order.append(\"load\")\n+            return []\n+\n+        with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n+             patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\", _record_reset), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\", _record_load), \\\n+             patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n+             patch(\n+                 \"hermes_cli.runtime_provider.resolve_runtime_provider\",\n+                 return_value={\n+                     \"api_key\": \"***\",\n+                     \"base_url\": \"https://example.invalid/v1\",\n+                     \"provider\": \"openrouter\",\n+                     \"api_mode\": \"chat_completions\",\n+                 },\n+             ), \\\n+             patch(\"run_agent.AIAgent\") as mock_agent_cls:\n+            mock_agent = MagicMock()\n+            mock_agent.run_conversation.return_value = {\"final_response\": \"ok\"}\n+            mock_agent_cls.return_value = mock_agent\n+            success, _output, _final, error = run_job(job)\n+\n+        assert success is True\n+        assert error is None\n+        # reset MUST precede the reload, else _APPLIED_HOMES no-ops the re-pull.\n+        assert call_order[:2] == [\"reset\", \"load\"], call_order\n+\n     def test_run_job_clears_stale_auto_delivery_thread_id_between_jobs(self, tmp_path, monkeypatch):\n         jobs = [\n             {","@@ -1709,7 +1767,8 @@ def test_bad_config_yaml_is_logged(self, caplog, tmp_path):\n         # (>30s wall clock) under load. See PR #33661 follow-up.\n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_cli.runtime_provider.resolve_runtime_provider\",\n                    return_value={\"provider\": \"openrouter\", \"api_key\": \"x\",\n                                  \"base_url\": \"https://example.invalid\",","@@ -1743,7 +1802,8 @@ def test_bad_prefill_messages_is_logged(self, caplog, tmp_path):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_cli.runtime_provider.resolve_runtime_provider\",\n                    return_value={\"provider\": \"openrouter\", \"api_key\": \"x\",\n                                  \"base_url\": \"https://example.invalid\",","@@ -1781,7 +1841,8 @@ def test_model_env_ref_in_config_yaml_is_expanded(self, tmp_path, monkeypatch):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\"hermes_cli.runtime_provider.resolve_runtime_provider\",\n                    return_value=self._RUNTIME), \\","@@ -1814,7 +1875,8 @@ def test_legacy_agent_prefill_messages_file_is_loaded(self, tmp_path, monkeypatc\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\"hermes_cli.runtime_provider.resolve_runtime_provider\",\n                    return_value=self._RUNTIME), \\","@@ -1844,7 +1906,8 @@ def test_fallback_model_env_ref_in_config_yaml_is_expanded(self, tmp_path, monke\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\"hermes_cli.runtime_provider.resolve_runtime_provider\",\n                    return_value=self._RUNTIME), \\","@@ -1873,7 +1936,8 @@ def test_unexpanded_ref_passthrough_when_var_unset(self, tmp_path, monkeypatch):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\"hermes_cli.runtime_provider.resolve_runtime_provider\",\n                    return_value=self._RUNTIME), \\","@@ -1917,7 +1981,8 @@ def test_null_job_model_falls_back_to_env(self, tmp_path, monkeypatch):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\"hermes_cli.runtime_provider.resolve_runtime_provider\",\n                    return_value=self._RUNTIME), \\","@@ -1941,7 +2006,8 @@ def test_null_job_model_falls_back_to_config_default(self, tmp_path, monkeypatch\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\"hermes_cli.runtime_provider.resolve_runtime_provider\",\n                    return_value=self._RUNTIME), \\","@@ -1973,7 +2039,8 @@ def test_explicit_null_model_block_in_config_does_not_overwrite_env(self, tmp_pa\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\"hermes_cli.runtime_provider.resolve_runtime_provider\",\n                    return_value=self._RUNTIME), \\","@@ -1996,7 +2063,8 @@ def test_no_model_anywhere_fails_with_actionable_error(self, tmp_path, monkeypat\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\"hermes_cli.runtime_provider.resolve_runtime_provider\",\n                    return_value=self._RUNTIME), \\","@@ -2025,7 +2093,8 @@ def test_job_model_update_takes_effect_on_next_run(self, tmp_path, monkeypatch):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\"hermes_cli.runtime_provider.resolve_runtime_provider\",\n                    return_value=self._RUNTIME), \\","@@ -2051,7 +2120,8 @@ def test_config_model_as_plain_string(self, tmp_path, monkeypatch):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\"hermes_cli.runtime_provider.resolve_runtime_provider\",\n                    return_value=self._RUNTIME), \\","@@ -2081,7 +2151,8 @@ def test_config_model_alias_key_resolves(self, tmp_path, monkeypatch):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\"hermes_cli.runtime_provider.resolve_runtime_provider\",\n                    return_value=self._RUNTIME), \\","@@ -2105,7 +2176,8 @@ def test_corrupt_config_yaml_does_not_crash_with_job_model(self, tmp_path, monke\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\"hermes_cli.runtime_provider.resolve_runtime_provider\",\n                    return_value=self._RUNTIME), \\","@@ -2147,7 +2219,8 @@ def _run_conversation(prompt):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\n                  \"hermes_cli.runtime_provider.resolve_runtime_provider\",","@@ -2207,7 +2280,8 @@ def _run_conversation(prompt):\n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n              patch(\"tools.credential_files._resolve_hermes_home\", return_value=tmp_path), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\n                  \"hermes_cli.runtime_provider.resolve_runtime_provider\",","@@ -2245,7 +2319,8 @@ def test_run_job_loads_skill_and_disables_recursive_cron_tools(self, tmp_path):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\n                  \"hermes_cli.runtime_provider.resolve_runtime_provider\",","@@ -2291,7 +2366,8 @@ def _skill_view(name):\n \n         with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n              patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n-             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.load_hermes_dotenv\"), \\\n+             patch(\"hermes_cli.env_loader.reset_secret_source_cache\"), \\\n              patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n              patch(\n                  \"hermes_cli.runtime_provider.resolve_runtime_provider\","]},{"file":"tests/hermes_cli/test_cron.py","hunks":["@@ -122,6 +122,23 @@ def test_list_does_not_crash_when_repeat_is_null(self, tmp_cron_dir, capsys):\n         out = capsys.readouterr().out\n         assert \"Repeat:    ∞\" in out\n \n+    def test_list_does_not_crash_when_deliver_is_null(self, tmp_cron_dir, capsys):\n+        \"\"\"A job can be persisted with ``\"deliver\": null`` (present-but-null).\n+        `cron list` must fall back to the default channel rather than crashing\n+        on ``\", \".join(None)`` — same dict-default pitfall as ``repeat`` (#32896).\n+        \"\"\"\n+        from cron.jobs import load_jobs, save_jobs\n+\n+        create_job(prompt=\"No deliver\", schedule=\"every 1h\")\n+        jobs = load_jobs()\n+        jobs[0][\"deliver\"] = None\n+        save_jobs(jobs)\n+\n+        cron_command(Namespace(cron_command=\"list\", all=True))\n+\n+        out = capsys.readouterr().out\n+        assert \"Deliver:   local\" in out\n+\n \n class TestGatewayNotRunningWarning:\n     \"\"\"`cron create` / `cron list` must warn when the gateway (and thus the\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}