# NousResearch/hermes-agent · Cron scheduler missing BSM secret resolution fixes

## Open now

- Nothing open on GitHub.

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.

## TL;DR

- 6 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #56144. Verify it addresses your case.
- **Recommended:** read PR #56144 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.33`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #33465: \[Bug\]: cron scheduler missing BSM resolution

## Prior & competing PRs

- **#33667: fix(cron): resolve BSM secrets in scheduler run_job (#33465)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#33465`, `#33479`, `#33527`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#56144: fix(cron): null-safe deliver in cron list + re-resolve BSM secrets per run**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `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:
  - `cron/scheduler.py`

    ```diff
    @@ -2225,12 +2225,23 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
     
         try:
             # Re-read .env and config.yaml fresh every run so provider/key
    -        # changes take effect without a gateway restart.
    -        from dotenv import load_dotenv
    -        try:
    -            load_dotenv(str(_get_hermes_home() / ".env"), override=True, encoding="utf-8")
    -        except UnicodeDecodeError:
    -            load_dotenv(str(_get_hermes_home() / ".env"), override=True, encoding="latin-1")
    +        # changes take effect without a gateway restart. Route through
    +        # load_hermes_dotenv (not a bare load_dotenv) and reset the secret-
    +        # source cache first: startup already applied external secrets and
    +        # recorded this HERMES_HOME in _APPLIED_HOMES, so a naive reload would
    +        # re-apply only the .env placeholder and never re-resolve a Bitwarden/
    +        # BSM-backed secret — leaving cron jobs 401'ing on the placeholder
    +        # (#33465). Clearing the cache forces the re-pull; the resolved secret
    +        # overrides the placeholder only when secrets.bitwarden.override_existing
    +        # is set (mirrors startup), and the Bitwarden value-cache keeps the
    +        # forced re-pull off the network. load_hermes_dotenv also handles the
    +        # utf-8/latin-1 encoding fallback internally.
    +        from hermes_cli.env_loader import (
    +            load_hermes_dotenv,
    +            reset_secret_source_cache,
    +        )
    +        reset_secret_source_cache()
    +        load_hermes_dotenv(hermes_home=_get_hermes_home())
     
             delivery_target = _resolve_delivery_target(job)
             if delivery_target:
    ```
  - `hermes_cli/cron.py`

    ```diff
    @@ -137,7 +137,11 @@ def cron_list(show_all: bool = False):
             repeat_completed = repeat_info.get("completed", 0)
             repeat_str = f"{repeat_completed}/{repeat_times}" if repeat_times else "∞"
     
    -        deliver = job.get("deliver", ["local"])
    +        # `deliver` may be present-but-null in the job record (same pitfall as
    +        # `repeat` above), so coalesce to the default rather than relying on the
    +        # dict-default, which only applies to a missing key. A null value would
    +        # otherwise reach `", ".join(None)` and crash the whole listing (#32896).
    +        deliver = job.get("deliver") or ["local"]
             if isinstance(deliver, str):
                 deliver = [deliver]
             deliver_str = ", ".join(deliver)
    ```
  - `tests/cron/test_cron_provider_pin.py`

    ```diff
    @@ -52,7 +52,8 @@ def _run_with_current_provider(job, current_provider, tmp_path):
         fake_db = MagicMock()
         with patch("cron.scheduler._hermes_home", tmp_path), \
              patch("cron.scheduler._resolve_origin", return_value=None), \
    -         patch("dotenv.load_dotenv"), \
    +         patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +         patch("hermes_cli.env_loader.reset_secret_source_cache"), \
              patch("hermes_state.SessionDB", return_value=fake_db), \
              patch(
                  "hermes_cli.runtime_provider.resolve_runtime_provider",
    ```

    ```diff
    @@ -252,7 +253,8 @@ def _run_with_current_provider_and_model(job, current_provider, current_model, t
         with patch("cron.scheduler._hermes_home", tmp_path), \
              patch("cron.scheduler._get_hermes_home", return_value=tmp_path), \
              patch("cron.scheduler._resolve_origin", return_value=None), \
    -         patch("dotenv.load_dotenv"), \
    +         patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +         patch("hermes_cli.env_loader.reset_secret_source_cache"), \
              patch("hermes_state.SessionDB", return_value=fake_db), \
              patch(
                  "hermes_cli.runtime_provider.resolve_runtime_provider",
    ```
  - `tests/cron/test_scheduler.py`

    ```diff
    @@ -966,7 +966,8 @@ def test_run_job_passes_session_db_and_cron_platform(self, tmp_path):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch(
                      "hermes_cli.runtime_provider.resolve_runtime_provider",
    ```

    ```diff
    @@ -1012,7 +1013,8 @@ def test_run_job_suppresses_empty_turn_explainer(self, tmp_path):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch(
                      "hermes_cli.runtime_provider.resolve_runtime_provider",
    ```

    ```diff
    @@ -1055,7 +1057,8 @@ def test_run_job_real_report_on_empty_reason_still_delivers(self, tmp_path):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch(
                      "hermes_cli.runtime_provider.resolve_runtime_provider",
    ```

    ```diff
    @@ -1095,7 +1098,8 @@ def test_run_job_titles_cron_session_from_job_not_important_hint(self, tmp_path)
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch(
                      "hermes_cli.runtime_provider.resolve_runtime_provider",
    ```

    ```diff
    @@ -1132,7 +1136,8 @@ def test_run_job_closes_agent_on_failure_to_prevent_fd_leak(self, tmp_path):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch(
                      "hermes_cli.runtime_provider.resolve_runtime_provider",
    ```

    ```diff
    @@ -1169,7 +1174,8 @@ def test_run_job_reaps_stale_auxiliary_clients_per_tick(self, tmp_path):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch(
                      "hermes_cli.runtime_provider.resolve_runtime_provider",
    ```

    ```diff
    @@ -1197,7 +1203,8 @@ def _make_run_job_patches(self, tmp_path):
             return fake_db, [
                 patch("cron.scheduler._hermes_home", tmp_path),
                 patch("cron.scheduler._resolve_origin", return_value=None),
    -            patch("dotenv.load_dotenv"),
    +            patch("hermes_cli.env_loader.load_hermes_dotenv"),
    +            patch("hermes_cli.env_loader.reset_secret_source_cache"),
                 patch("hermes_state.SessionDB", return_value=fake_db),
                 patch(
                     "hermes_cli.runtime_provider.resolve_runtime_provider",
    ```

    ```diff
    @@ -1218,7 +1225,7 @@ def test_run_job_passes_enabled_toolsets_to_agent(self, tmp_path):
                 "enabled_toolsets": ["web", "terminal", "file"],
             }
             fake_db, patches = self._make_run_job_patches(tmp_path)
    -        with patches[0], patches[1], patches[2], patches[3], patches[4], \
    +        with patches[0], patches[1], patches[2], patches[3], patches[4], patches[5], \
                  patch("run_agent.AIAgent") as mock_agent_cls:
                 mock_agent = MagicMock()
                 mock_agent.run_conversation.return_value = {"final_response": "ok"}
    ```

    ```diff
    @@ -1252,7 +1259,7 @@ def test_run_job_disabled_toolsets_layer_user_config_on_baseline(self, tmp_path)
                 "enabled_toolsets": ["web", "terminal", "file"],
             }
             fake_db, patches = self._make_run_job_patches(tmp_path)
    -        with patches[0], patches[1], patches[2], patches[3], patches[4], \
    +        with patches[0], patches[1], patches[2], patches[3], patches[4], patches[5], \
                  patch("run_agent.AIAgent") as mock_agent_cls:
                 mock_agent = MagicMock()
                 mock_agent.run_conversation.return_value = {"final_response": "ok"}
    ```

    ```diff
    @@ -1279,7 +1286,7 @@ def test_run_job_enabled_toolsets_resolves_from_platform_config_when_not_set(sel
                 "prompt": "hello",
             }
             fake_db, patches = self._make_run_job_patches(tmp_path)
    -        with patches[0], patches[1], patches[2], patches[3], patches[4], \
    +        with patches[0], patches[1], patches[2], patches[3], patches[4], patches[5], \
                  patch("run_agent.AIAgent") as mock_agent_cls:
                 mock_agent = MagicMock()
                 mock_agent.run_conversation.return_value = {"final_response": "ok"}
    ```

    ```diff
    @@ -1307,7 +1314,7 @@ def test_run_job_per_job_toolsets_win_over_platform_config(self, tmp_path):
             fake_db, patches = self._make_run_job_patches(tmp_path)
             # Even if the user has ``hermes tools`` configured to enable web+file
             # for cron, the per-job override wins.
    -        with patches[0], patches[1], patches[2], patches[3], patches[4], \
    +        with patches[0], patches[1], patches[2], patches[3], patches[4], patches[5], \
                  patch("run_agent.AIAgent") as mock_agent_cls, \
                  patch(
                      "hermes_cli.tools_config._get_platform_tools",
    ```

    ```diff
    @@ -1336,7 +1343,8 @@ def test_run_job_empty_response_returns_empty_not_placeholder(self, tmp_path):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch(
                      "hermes_cli.runtime_provider.resolve_runtime_provider",
    ```

    ```diff
    @@ -1412,7 +1420,8 @@ def test_run_job_treats_agent_failure_flag_as_failure(
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch(
                      "hermes_cli.runtime_provider.resolve_runtime_provider",
    ```

    ```diff
    @@ -1451,7 +1460,8 @@ def test_run_job_completed_true_without_failed_flag_succeeds(self, tmp_path):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch(
                      "hermes_cli.runtime_provider.resolve_runtime_provider",
    ```

    ```diff
    @@ -1493,7 +1503,8 @@ def test_run_job_delivers_max_iteration_fallback_summary(self, tmp_path):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch(
                      "hermes_cli.runtime_provider.resolve_runtime_provider",
    ```

    ```diff
    @@ -1613,6 +1624,53 @@ def run_conversation(self, *args, **kwargs):
             assert os.getenv("HERMES_CRON_AUTO_DELIVER_THREAD_ID") is None
             fake_db.close.assert_called_once()
     
    +    def test_run_job_resets_secret_source_cache_before_reload(self, tmp_path, monkeypatch):
    +        """Each run must clear the secret-source cache before re-reading the
    +        env, so a long-running gateway re-resolves Bitwarden/BSM-backed secrets
    +        instead of leaving the startup .env placeholder in place (#33465).
    +
    +        A bare ``load_dotenv`` re-load can't do this: startup already recorded
    +        this HERMES_HOME in ``_APPLIED_HOMES``, so the external-secret pull
    +        no-ops and only the placeholder is re-applied. The scheduler must call
    +        ``reset_secret_source_cache()`` (forcing the re-pull) and route through
    +        ``load_hermes_dotenv`` (which then re-applies external secret sources).
    +        """
    +        job = {"id": "bsm-job", "name": "bsm", "prompt": "hello"}
    +        fake_db = MagicMock()
    +        call_order = []
    +
    +        def _record_reset():
    +            call_order.append("reset")
    +
    +        def _record_load(*args, **kwargs):
    +            call_order.append("load")
    +            return []
    +
    +        with patch("cron.scheduler._hermes_home", tmp_path), \
    +             patch("cron.scheduler._resolve_origin", return_value=None), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache", _record_reset), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv", _record_load), \
    +             patch("hermes_state.SessionDB", return_value=fake_db), \
    +             patch(
    +                 "hermes_cli.runtime_provider.resolve_runtime_provider",
    +                 return_value={
    +                     "api_key": "***",
    +                     "base_url": "https://example.invalid/v1",
    +                     "provider": "openrouter",
    +                     "api_mode": "chat_completions",
    +                 },
    +             ), \
    +             patch("run_agent.AIAgent") as mock_agent_cls:
    +            mock_agent = MagicMock()
    +            mock_agent.run_conversation.return_value = {"final_response": "ok"}
    +            mock_agent_cls.return_value = mock_agent
    +            success, _output, _final, error = run_job(job)
    +
    +        assert success is True
    +        assert error is None
    +        # reset MUST precede the reload, else _APPLIED_HOMES no-ops the re-pull.
    +        assert call_order[:2] == ["reset", "load"], call_order
    +
         def test_run_job_clears_stale_auto_delivery_thread_id_between_jobs(self, tmp_path, monkeypatch):
             jobs = [
                 {
    ```

    ```diff
    @@ -1709,7 +1767,8 @@ def test_bad_config_yaml_is_logged(self, caplog, tmp_path):
             # (>30s wall clock) under load. See PR #33661 follow-up.
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_cli.runtime_provider.resolve_runtime_provider",
                        return_value={"provider": "openrouter", "api_key": "x",
                                      "base_url": "https://example.invalid",
    ```

    ```diff
    @@ -1743,7 +1802,8 @@ def test_bad_prefill_messages_is_logged(self, caplog, tmp_path):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_cli.runtime_provider.resolve_runtime_provider",
                        return_value={"provider": "openrouter", "api_key": "x",
                                      "base_url": "https://example.invalid",
    ```

    ```diff
    @@ -1781,7 +1841,8 @@ def test_model_env_ref_in_config_yaml_is_expanded(self, tmp_path, monkeypatch):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch("hermes_cli.runtime_provider.resolve_runtime_provider",
                        return_value=self._RUNTIME), \
    ```

    ```diff
    @@ -1814,7 +1875,8 @@ def test_legacy_agent_prefill_messages_file_is_loaded(self, tmp_path, monkeypatc
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch("hermes_cli.runtime_provider.resolve_runtime_provider",
                        return_value=self._RUNTIME), \
    ```

    ```diff
    @@ -1844,7 +1906,8 @@ def test_fallback_model_env_ref_in_config_yaml_is_expanded(self, tmp_path, monke
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch("hermes_cli.runtime_provider.resolve_runtime_provider",
                        return_value=self._RUNTIME), \
    ```

    ```diff
    @@ -1873,7 +1936,8 @@ def test_unexpanded_ref_passthrough_when_var_unset(self, tmp_path, monkeypatch):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch("hermes_cli.runtime_provider.resolve_runtime_provider",
                        return_value=self._RUNTIME), \
    ```

    ```diff
    @@ -1917,7 +1981,8 @@ def test_null_job_model_falls_back_to_env(self, tmp_path, monkeypatch):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch("hermes_cli.runtime_provider.resolve_runtime_provider",
                        return_value=self._RUNTIME), \
    ```

    ```diff
    @@ -1941,7 +2006,8 @@ def test_null_job_model_falls_back_to_config_default(self, tmp_path, monkeypatch
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch("hermes_cli.runtime_provider.resolve_runtime_provider",
                        return_value=self._RUNTIME), \
    ```

    ```diff
    @@ -1973,7 +2039,8 @@ def test_explicit_null_model_block_in_config_does_not_overwrite_env(self, tmp_pa
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch("hermes_cli.runtime_provider.resolve_runtime_provider",
                        return_value=self._RUNTIME), \
    ```

    ```diff
    @@ -1996,7 +2063,8 @@ def test_no_model_anywhere_fails_with_actionable_error(self, tmp_path, monkeypat
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch("hermes_cli.runtime_provider.resolve_runtime_provider",
                        return_value=self._RUNTIME), \
    ```

    ```diff
    @@ -2025,7 +2093,8 @@ def test_job_model_update_takes_effect_on_next_run(self, tmp_path, monkeypatch):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch("hermes_cli.runtime_provider.resolve_runtime_provider",
                        return_value=self._RUNTIME), \
    ```

    ```diff
    @@ -2051,7 +2120,8 @@ def test_config_model_as_plain_string(self, tmp_path, monkeypatch):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch("hermes_cli.runtime_provider.resolve_runtime_provider",
                        return_value=self._RUNTIME), \
    ```

    ```diff
    @@ -2081,7 +2151,8 @@ def test_config_model_alias_key_resolves(self, tmp_path, monkeypatch):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch("hermes_cli.runtime_provider.resolve_runtime_provider",
                        return_value=self._RUNTIME), \
    ```

    ```diff
    @@ -2105,7 +2176,8 @@ def test_corrupt_config_yaml_does_not_crash_with_job_model(self, tmp_path, monke
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch("hermes_cli.runtime_provider.resolve_runtime_provider",
                        return_value=self._RUNTIME), \
    ```

    ```diff
    @@ -2147,7 +2219,8 @@ def _run_conversation(prompt):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch(
                      "hermes_cli.runtime_provider.resolve_runtime_provider",
    ```

    ```diff
    @@ -2207,7 +2280,8 @@ def _run_conversation(prompt):
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
                  patch("tools.credential_files._resolve_hermes_home", return_value=tmp_path), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch(
                      "hermes_cli.runtime_provider.resolve_runtime_provider",
    ```

    ```diff
    @@ -2245,7 +2319,8 @@ def test_run_job_loads_skill_and_disables_recursive_cron_tools(self, tmp_path):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch(
                      "hermes_cli.runtime_provider.resolve_runtime_provider",
    ```

    ```diff
    @@ -2291,7 +2366,8 @@ def _skill_view(name):
     
             with patch("cron.scheduler._hermes_home", tmp_path), \
                  patch("cron.scheduler._resolve_origin", return_value=None), \
    -             patch("dotenv.load_dotenv"), \
    +             patch("hermes_cli.env_loader.load_hermes_dotenv"), \
    +             patch("hermes_cli.env_loader.reset_secret_source_cache"), \
                  patch("hermes_state.SessionDB", return_value=fake_db), \
                  patch(
                      "hermes_cli.runtime_provider.resolve_runtime_provider",
    ```
  - `tests/hermes_cli/test_cron.py`

    ```diff
    @@ -122,6 +122,23 @@ def test_list_does_not_crash_when_repeat_is_null(self, tmp_cron_dir, capsys):
             out = capsys.readouterr().out
             assert "Repeat:    ∞" in out
     
    +    def test_list_does_not_crash_when_deliver_is_null(self, tmp_cron_dir, capsys):
    +        """A job can be persisted with ``"deliver": null`` (present-but-null).
    +        `cron list` must fall back to the default channel rather than crashing
    +        on ``", ".join(None)`` — same dict-default pitfall as ``repeat`` (#32896).
    +        """
    +        from cron.jobs import load_jobs, save_jobs
    +
    +        create_job(prompt="No deliver", schedule="every 1h")
    +        jobs = load_jobs()
    +        jobs[0]["deliver"] = None
    +        save_jobs(jobs)
    +
    +        cron_command(Namespace(cron_command="list", all=True))
    +
    +        out = capsys.readouterr().out
    +        assert "Deliver:   local" in out
    +
     
     class TestGatewayNotRunningWarning:
         """`cron create` / `cron list` must warn when the gateway (and thus the
    ```
- **#33479: fix(cron): pull Bitwarden Secrets Manager secrets in run_job**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#33527: fix(cron): harden ticker thread, null-safe deliver, and BSM env loader**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#56144`
- **#34041: fix(cron): use load_hermes_dotenv so BSM secrets resolve for cron jobs**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `836732f54f7235d8cbae01f5cd4e1b86b0b70b49`, `#33465`, `#56144`

## Fix status

- Merged to main in PR #56144 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-01`.
- Subsystem: `cron`
