# NousResearch/hermes-agent · CLI yolo mode approval bypass not working correctly

## Open now

- Nothing open on GitHub.

Multiple bug reports and fix attempts addressing the --yolo flag and /yolo chat command failing to bypass dangerous command approvals, due to environment variable timing issues relative to plugin discovery and missing session-level enable calls.

## TL;DR

- 8 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 2 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #33931. Verify it addresses your case.
- **Recommended:** read PR #33931 (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: `resolved` · deciding PR(s): `pr-33931,pr-61715`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.23`: 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

- #33925: CLI /yolo (in-chat) does not bypass dangerous command approvals — env var freeze + missing enable_session_yolo call
- #60328: \[Bug\]: hermes --yolo broken — approval bypass silently ignored since v0.11.0

## Prior & competing PRs

- **#33931: fix(cli): /yolo in chat must enable session bypass, not just set env var**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `cli.py`, `tests/cli/test_cli_yolo_toggle.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -168,7 +168,7 @@ def realign_markdown_tables(*args, **kwargs):
         try_launch_chrome_debug,
     )
     from hermes_cli.env_loader import load_hermes_dotenv
    -from utils import base_url_host_matches, is_truthy_value
    +from utils import base_url_host_matches
     
     _hermes_home = get_hermes_home()
     _project_env = Path(__file__).parent / '.env'
    ```

    ```diff
    @@ -3747,7 +3747,7 @@ def _build_status_bar_text(self, width: Optional[int] = None) -> str:
                 percent_label = f"{percent}%" if percent is not None else "--"
                 duration_label = snapshot["duration"]
     
    -            yolo_active = bool(os.getenv("HERMES_YOLO_MODE"))
    +            yolo_active = self._is_session_yolo_active()
                 if width < 52:
                     text = f"⚕ {snapshot['model_short']} · {duration_label}"
                     if yolo_active:
    ```

    ```diff
    @@ -3808,7 +3808,7 @@ def _get_status_bar_fragments(self):
                 # line and produce duplicated status bar rows over long sessions.
                 width = self._get_tui_terminal_width()
                 duration_label = snapshot["duration"]
    -            yolo_active = bool(os.getenv("HERMES_YOLO_MODE"))
    +            yolo_active = self._is_session_yolo_active()
     
                 if width < 52:
                     frags = [
    ```

    ```diff
    @@ -6907,6 +6907,7 @@ def _handle_branch_command(self, cmd_original: str) -> None:
                 pass
     
             # Switch to the new session
    +        self._transfer_session_yolo(self.session_id, new_session_id)
             self.session_id = new_session_id
             self.session_start = now
             self._pending_title = None
    ```

    ```diff
    @@ -9607,20 +9608,92 @@ def _toggle_verbose(self):
             }
             _cprint(labels.get(self.tool_progress_mode, ""))
     
    +    def _transfer_session_yolo(self, old_session_id: str, new_session_id: str) -> None:
    +        """Move YOLO bypass state from an old session key to a new one.
    +
    +        Called whenever ``self.session_id`` is reassigned mid-run — ``/branch``
    +        forks into a new session, and auto-compression rotates the agent's
    +        session id into a fresh continuation session. Without this transfer
    +        the user's ``/yolo ON`` toggle would silently revert on the very next
    +        turn (the same UX failure mode that motivated this entire fix), since
    +        ``_session_yolo`` is keyed by session id.
    +
    +        Mirrors ``tui_gateway/server.py`` (~line 1297-1305) which performs the
    +        same transfer for the TUI's session-rename path. No-op when YOLO
    +        wasn't enabled or when the ids match.
    +        """
    +        if not old_session_id or not new_session_id or old_session_id == new_session_id:
    +            return
    +        try:
    +            from tools.approval import (
    +                disable_session_yolo,
    +                enable_session_yolo,
    +                is_session_yolo_enabled,
    +            )
    +        except Exception:
    +            return
    +        if is_session_yolo_enabled(old_session_id):
    +            enable_session_yolo(new_session_id)
    +            disable_session_yolo(old_session_id)
    +
    +    def _is_session_yolo_active(self) -> bool:
    +        """Whether YOLO bypass is currently enabled for this CLI session.
    +
    +        Reads from ``tools.approval._session_yolo`` (the same set that
    +        ``enable_session_yolo`` / ``disable_session_yolo`` write to) so the
    +        status bar reflects the actual bypass state instead of a stale env
    +        var. Also honors the process-start ``--yolo`` flag, which freezes
    +        ``HERMES_YOLO_MODE`` into ``_YOLO_MODE_FROZEN`` before tool imports
    +        happen.
    +        """
    +        try:
    +            from tools.approval import (
    +                _YOLO_MODE_FROZEN,
    +                is_session_yolo_enabled,
    +            )
    +        except Exception:
    +            return False
    +        if _YOLO_MODE_FROZEN:
    +            return True
    +        # Use ``getattr`` so test fixtures that build a CLI via ``__new__``
    +        # (skipping ``__init__``) don't trip an AttributeError here; the
    +        # status-bar builders swallow exceptions silently but lose every
    +        # field after the failure.
    +        session_key = getattr(self, "session_id", None) or "default"
    +        return is_session_yolo_enabled(session_key)
    +
         def _toggle_yolo(self):
    -        """Toggle YOLO mode — skip all dangerous command approval prompts."""
    -        import os
    +        """Toggle YOLO mode — skip all dangerous command approval prompts.
    +
    +        Per-session toggle that mirrors the gateway and TUI ``/yolo`` handlers
    +        (see ``gateway/run.py:_handle_yolo_command`` and
    +        ``tui_gateway/server.py`` key=="yolo"). We deliberately do NOT mutate
    +        ``HERMES_YOLO_MODE`` here — that env var is read once at module import
    +        time into ``tools.approval._YOLO_MODE_FROZEN`` to keep prompt-injected
    +        skills from flipping the bypass mid-session, so setting it after CLI
    +        startup is a silent no-op. Routing through ``enable_session_yolo`` /
    +        ``disable_session_yolo`` gives the same auditable, per-session bypass
    +        the other surfaces have. ``run_conversation`` binds
    +        ``self.session_id`` as the active approval session key via
    +        ``set_current_session_key`` so the bypass takes effect on the very
    +        next dangerous command in this run.
    +        """
             from hermes_cli.colors import Colors as _Colors
    +        from tools.approval import (
    +            disable_session_yolo,
    +            enable_session_yolo,
    +            is_session_yolo_enabled,
    +        )
     
    -        current = is_truthy_value(os.environ.get("HERMES_YOLO_MODE"))
    -        if current:
    -            os.environ.pop("HERMES_YOLO_MODE", None)
    +        session_key = self.session_id or "default"
    +        if is_session_yolo_enabled(session_key):
    +            disable_session_yolo(session_key)
                 _cprint(
                     f"  ⚠ YOLO mode {_Colors.BOLD}{_Colors.RED}OFF{_Colors.RESET}"
                     " — dangerous commands will require approval."
                 )
             else:
    -            os.environ["HERMES_YOLO_MODE"] = "1"
    +            enable_session_yolo(session_key)
                 _cprint(
                     f"  ⚡ YOLO mode {_Colors.BOLD}{_Colors.GREEN}ON{_Colors.RESET}"
                     " — all commands auto-approved. Use with caution."
    ```

    ```diff
    @@ -11757,6 +11830,23 @@ def run_agent():
                         set_secret_capture_callback(self._secret_capture_callback)
                     except Exception:
                         pass
    +                # Bind this turn's approval session key into the contextvar so
    +                # ``tools.approval.is_current_session_yolo_enabled()`` resolves
    +                # against the same key that ``/yolo`` toggles under (see
    +                # ``_toggle_yolo`` → ``enable_session_yolo(self.session_id)``).
    +                # Mirrors ``tui_gateway/server.py`` and ``gateway/run.py`` which
    +                # bind the same contextvar before invoking the agent.
    +                try:
    +                    from tools.approval import (
    +                        reset_current_session_key,
    +                        set_current_session_key,
    +                    )
    +                    _approval_session_token = [redacted:secret-assignment]
    +                        self.session_id or "default"
    +                    )
    +                except Exception:
    +                    reset_current_session_key = None  # type: ignore[assignment]
    +                    _approval_session_token = None
                     agent_message = _voice_prefix + message if _voice_prefix else message
                     # Prepend pending model switch note so the model knows about the switch
                     _msn = getattr(self, '_pending_model_switch_note', None)
    ```

    ```diff
    @@ -11798,6 +11888,15 @@ def run_agent():
                             set_secret_capture_callback(None)
                         except Exception:
                             pass
    +                    # Release the per-turn approval session key. ``_session_yolo``
    +                    # state itself is preserved across turns (so /yolo persists
    +                    # for the whole CLI run); we just unbind the contextvar so a
    +                    # reused thread doesn't see stale identity on its next run.
    +                    if _approval_session_token is not None and reset_current_session_key is not None:
    +                        try:
    +                            reset_current_session_key(_approval_session_token)
    +                        except Exception:
    +                            pass
     
                 # Start agent in background thread (daemon so it cannot keep the
                 # process alive when the user closes the terminal tab — SIGHUP
    ```

    ```diff
    @@ -11928,6 +12027,7 @@ def run_agent():
                     and getattr(self.agent, "session_id", None)
                     and self.agent.session_id != self.session_id
                 ):
    +                self._transfer_session_yolo(self.session_id, self.agent.session_id)
                     self.session_id = self.agent.session_id
                     self._pending_title = None
     
    ```
  - `tests/cli/test_cli_yolo_toggle.py`

    ```diff
    @@ -0,0 +1,244 @@
    +"""Regression tests for the CLI ``/yolo`` in-chat toggle.
    +
    +Pre-fix bug (issue #33925): ``cli.HermesCLI._toggle_yolo`` mutated only
    +``os.environ["HERMES_YOLO_MODE"]``. That env var is captured once at
    +module-import time into ``tools.approval._YOLO_MODE_FROZEN`` (security
    +hardening: stops prompt-injected skills from flipping the bypass mid-run),
    +so the post-startup toggle was a silent no-op. ``/yolo`` advertised "YOLO ON"
    +in the status bar while every dangerous command still hit the approval
    +prompt. Only ``hermes --yolo`` (process-start env), ``HERMES_YOLO_MODE=1``,
    +and ``hermes config set approvals.mode off`` actually bypassed.
    +
    +The fix routes the CLI toggle through ``enable_session_yolo`` /
    +``disable_session_yolo`` (matching the gateway and TUI ``/yolo`` paths) and
    +binds ``self.session_id`` as the active approval session key around each
    +``run_conversation`` call so ``is_current_session_yolo_enabled()`` resolves
    +against the same key the toggle writes under.
    +
    +We test ``_toggle_yolo`` and ``_is_session_yolo_active`` as unbound methods
    +against a minimal stand-in object that exposes only the attribute they
    +read (``session_id``). This avoids the heavy ``HermesCLI`` construction
    +path used in ``test_cli_init.py``, which is incompatible with this test
    +file's path layout — ``HermesCLI.__init__`` imports a lot of optional
    +state we don't need here.
    +"""
    +
    +import os
    +from types import SimpleNamespace
    +from unittest.mock import patch
    +
    +import pytest
    +
    +import tools.approval as approval_module
    +from cli import HermesCLI
    +
    +
    +SESSION_KEY = "test-cli-yolo-session"
    +
    +
    [redacted:email](autouse=True)
    +def _clear_approval_state(monkeypatch):
    +    """Clear the YOLO bypass + env var around every test so cases are independent."""
    +    monkeypatch.delenv("HERMES_YOLO_MODE", raising=False)
    +    approval_module.clear_session(SESSION_KEY)
    +    approval_module.clear_session("default")
    +    yield
    +    approval_module.clear_session(SESSION_KEY)
    +    approval_module.clear_session("default")
    +
    +
    +def _make_stand_in(session_id: str = SESSION_KEY) -> SimpleNamespace:
    +    """Minimal stand-in exposing only ``session_id``.
    +
    +    ``_toggle_yolo`` and ``_is_session_yolo_active`` are both pure methods
    +    that only read ``self.session_id`` — no other CLI state is touched.
    +    Calling them as unbound functions against this stand-in is equivalent
    +    to invoking them on a fully-constructed ``HermesCLI`` for the
    +    behaviour under test, and avoids the brittle prompt_toolkit / config
    +    stubbing required to instantiate ``HermesCLI`` from this test file.
    +    """
    +    return SimpleNamespace(session_id=session_id)
    +
    +
    +class TestToggleYoloIsSessionScoped:
    +    """The CLI /yolo handler must mutate the session-yolo set, not the env var.
    +
    +    The env var path is dead-on-arrival because ``_YOLO_MODE_FROZEN`` is
    +    captured once at module import, long before the CLI's ``/yolo`` command
    +    can run.
    +    """
    +
    +    def test_toggle_yolo_enables_session_bypass(self):
    +        stand_in = _make_stand_in()
    +
    +        assert approval_module.is_session_yolo_enabled(SESSION_KEY) is False
    +
    +        with patch("cli._cprint"):
    +            HermesCLI._toggle_yolo(stand_in)
    +
    +        assert approval_module.is_session_yolo_enabled(SESSION_KEY) is True
    +
    +    def test_toggle_yolo_disables_session_bypass_on_second_call(self):
    +        stand_in = _make_stand_in()
    +        with patch("cli._cprint"):
    +            HermesCLI._toggle_yolo(stand_in)  # ON
    +            assert approval_module.is_session_yolo_enabled(SESSION_KEY) is True
    +            HermesCLI._toggle_yolo(stand_in)  # OFF
    +            assert approval_module.is_session_yolo_enabled(SESSION_KEY) is False
    +
    +    def test_toggle_yolo_does_not_mutate_env_var(self):
    +        """Toggling /yolo must not write ``HERMES_YOLO_MODE`` — that path is
    +        frozen at import time and would mislead anyone reading the env later
    +        (subprocesses, status bars wired to the env, the relaunch flag list)."""
    +        stand_in = _make_stand_in()
    +        with patch("cli._cprint"):
    +            HermesCLI._toggle_yolo(stand_in)
    +
    +        assert os.environ.get("HERMES_YOLO_MODE") is None
    +
    +    def test_toggle_yolo_falls_back_to_default_when_session_id_missing(self):
    +        """An edge case during CLI bootstrap: a ``/yolo`` triggered before the
    +        session id is set should not blow up, and should land under the
    +        ``default`` session key so the bypass still takes effect for any code
    +        that resolves against the default key."""
    +        stand_in = _make_stand_in(session_id="")
    +        with patch("cli._cprint"):
    +            HermesCLI._toggle_yolo(stand_in)
    +
    +        assert approval_module.is_session_yolo_enabled("default") is True
    +
    +    def test_two_independent_sessions_are_isolated(self):
    +        """``/yolo`` toggled in one session must not bypass approvals in
    +        another session — mirrors the gateway-side invariant."""
    +        cli_a = _make_stand_in(session_id="session-yolo-a")
    +        cli_b = _make_stand_in(session_id="session-yolo-b")
    +
    +        try:
    +            with patch("cli._cprint"):
    +                HermesCLI._toggle_yolo(cli_a)
    +
    +            assert approval_module.is_session_yolo_enabled("session-yolo-a") is True
    +            assert approval_module.is_session_yolo_enabled("session-yolo-b") is False
    +        finally:
    +            approval_module.clear_session("session-yolo-a")
    +            approval_module.clear_session("session-yolo-b")
    +
    +
    +class TestIsSessionYoloActiveHelper:
    +    """The status-bar helper must read the live session-yolo state, not the
    +    env var (which is the bug class this PR fixes)."""
    +
    +    def test_helper_reflects_toggle(self):
    +        stand_in = _make_stand_in()
    +
    +        assert HermesCLI._is_session_yolo_active(stand_in) is False
    +
    +        with patch("cli._cprint"):
    +            HermesCLI._toggle_yolo(stand_in)
    +
    +        assert HermesCLI._is_session_yolo_active(stand_in) is True
    +
    +        with patch("cli._cprint"):
    +            HermesCLI._toggle_yolo(stand_in)
    +
    +        assert HermesCLI._is_session_yolo_active(stand_in) is False
    +
    +    def test_helper_honors_frozen_yolo_mode(self):
    +        """``hermes --yolo`` sets ``HERMES_YOLO_MODE`` before tool imports, so
    +        ``_YOLO_MODE_FROZEN`` ends up True. The status bar should still
    +        reflect YOLO on in that case even when the session toggle is off."""
    +        stand_in = _make_stand_in()
    +
    +        with patch.object(approval_module, "_YOLO_MODE_FROZEN", True):
    +            assert HermesCLI._is_session_yolo_active(stand_in) is True
    +
    +
    +class TestToggleYoloEndToEnd:
    +    """End-to-end: a dangerous command must auto-approve through the same
    +    ``check_all_command_guards`` path the terminal tool uses."""
    +
    +    def test_toggle_yolo_bypasses_dangerous_command_check(self):
    +        stand_in = _make_stand_in()
    +
    +        token = [redacted:secret-assignment]
    +        try:
    +            with patch("cli._cprint"):
    +                HermesCLI._toggle_yolo(stand_in)  # YOLO ON
    +
    +            result = approval_module.check_all_command_guards(
    +                "rm -rf /tmp/scratch-xyzzy", "local",
    +            )
    +            assert result["approved"] is True, (
    +                f"YOLO toggle should auto-approve dangerous commands, got: {result}"
    +            )
    +        finally:
    +            approval_module.reset_current_session_key(token)
    +
    +
    +class TestIsSessionYoloActiveAttrSafety:
    +    """The status-bar helper runs against partially-constructed CLI fixtures
    +    (tests use ``HermesCLI.__new__(HermesCLI)`` to skip ``__init__``). It must
    +    not raise ``AttributeError`` when ``session_id`` is absent — the
    +    status-bar builders swallow exceptions silently and lose every field
    +    after the failure, producing a regression that's hard to track back to
    +    the helper."""
    +
    +    def test_helper_survives_missing_session_id_attr(self):
    +        # SimpleNamespace WITHOUT session_id mimics __new__-built fixtures.
    +        from types import SimpleNamespace
    +        no_attr = SimpleNamespace()
    +        # Must return False, not raise.
    +        assert HermesCLI._is_session_yolo_active(no_attr) is False
    +
    +
    +class TestSessionRotationTransfersYolo:
    +    """When the CLI's ``session_id`` rotates mid-run (``/branch``, auto
    +    compression continuation), YOLO state keyed under the old id must move
    +    to the new id. Otherwise the user's ``/yolo ON`` silently reverts on
    +    the next turn — the same UX failure mode this PR set out to fix.
    +    Mirrors ``tui_gateway/server.py`` ~line 1297-1305."""
    +
    +    def test_transfer_moves_yolo_to_new_session(self):
    +        stand_in = _make_stand_in(session_id="old-id")
    +        try:
    +            approval_module.enable_session_yolo("old-id")
    +            assert approval_module.is_session_yolo_enabled("old-id") is True
    +
    +            HermesCLI._transfer_session_yolo(stand_in, "old-id", "new-id")
    +
    +            assert approval_module.is_session_yolo_enabled("new-id") is True
    +            assert approval_module.is_session_yolo_enabled("old-id") is False
    +        finally:
    +            approval_module.clear_session("old-id")
    +            approval_module.clear_session("new-id")
    +
    +    def test_transfer_is_noop_when_yolo_was_off(self):
    +        stand_in = _make_stand_in(session_id="old-id")
    +        try:
    +            HermesCLI._transfer_session_yolo(stand_in, "old-id", "new-id")
    +            assert approval_module.is_session_yolo_enabled("new-id") is False
    +            assert approval_module.is_session_yolo_enabled("old-id") is False
    +        finally:
    +            approval_module.clear_session("old-id")
    +            approval_module.clear_session("new-id")
    +
    +    def test_transfer_is_noop_when_ids_match(self):
    +        stand_in = _make_stand_in(session_id="same-id")
    +        try:
    +            approval_module.enable_session_yolo("same-id")
    +            HermesCLI._transfer_session_yolo(stand_in, "same-id", "same-id")
    +            # Must NOT have been disabled — same-id == same-id is a no-op,
    +            # not a "disable then re-enable" round-trip.
    +            assert approval_module.is_session_yolo_enabled("same-id") is True
    +        finally:
    +            approval_module.clear_session("same-id")
    +
    +    def test_transfer_handles_empty_inputs_safely(self):
    +        stand_in = _make_stand_in(session_id="x")
    +        # Both directions of empty input should be safe no-ops; nothing
    +        # to transfer from "" / to "".
    +        HermesCLI._transfer_session_yolo(stand_in, "", "new")
    +        HermesCLI._transfer_session_yolo(stand_in, "old", "")
    +        # Neither key should have been touched.
    +        assert approval_module.is_session_yolo_enabled("new") is False
    +        assert approval_module.is_session_yolo_enabled("old") is False
    ```
- **#60349: fix(cli): set HERMES_YOLO_MODE before _prepare_agent_startup() imports (#60328)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#60360`, `#60360`, `#61715`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#61715: fix(cli): set HERMES_YOLO_MODE before plugin discovery freezes the approval bypass (#60328)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/main.py`, `scripts/release.py`, `tests/hermes_cli/test_yolo_startup_order.py`
  - fix hunks:
  - `hermes_cli/main.py`

    ```diff
    @@ -2358,7 +2358,11 @@ def cmd_chat(args):
         except Exception:
             pass
     
    -    # --yolo: bypass all dangerous command approvals
    +    # --yolo: bypass all dangerous command approvals.
    +    # Also set in main() before _prepare_agent_startup() — that is the
    +    # authoritative site because it runs before tool imports freeze
    +    # _YOLO_MODE_FROZEN.  This redundant set is a safety net for callers
    +    # that invoke cmd_chat directly (e.g. subcommand dispatch).
         if getattr(args, "yolo", False):
             os.environ["HERMES_YOLO_MODE"] = "1"
     
    ```

    ```diff
    @@ -12410,6 +12414,15 @@ def _should_background_mcp_startup(args) -> bool:
     
     def _prepare_agent_startup(args) -> None:
         """Discover plugins/MCP/hooks for commands that can run an agent turn."""
    +    # --yolo: chokepoint guarantee that HERMES_YOLO_MODE is set before ANY
    +    # plugin/tool discovery below imports tools.approval, which freezes
    +    # _YOLO_MODE_FROZEN at import time (PR #7994 security design).  main()'s
    +    # dispatch path also sets this earlier, but _prepare_agent_startup() is
    +    # reachable from other launchers too (e.g. the Termux fast-CLI path),
    +    # so the guarantee lives here where the import is actually triggered
    +    # (#60328).
    +    if getattr(args, "yolo", False):
    +        os.environ["HERMES_YOLO_MODE"] = "1"
         _apply_safe_mode(args)
     
         _sub_attr, _sub_set = _AGENT_SUBCOMMANDS.get(args.command, (None, None))
    ```

    ```diff
    @@ -14627,6 +14640,15 @@ def _export_one(session_id: str):
             cmd_version(args)
             return
     
    +    # --yolo: set HERMES_YOLO_MODE *before* plugin discovery.  The call to
    +    # _prepare_agent_startup() below triggers discover_plugins() → tool
    +    # imports, and tools.approval freezes _YOLO_MODE_FROZEN at module
    +    # import time (PR #7994, security hardening against prompt-injection).
    +    # If the env var is set only later (e.g. inside cmd_chat), the frozen
    +    # value is already False and --yolo silently does nothing.
    +    if getattr(args, "yolo", False):
    +        os.environ["HERMES_YOLO_MODE"] = "1"
    +
         # Discover Python plugins and register shell hooks once, before any
         # command that can fire lifecycle hooks.  Both are idempotent; gated
         # so introspection/management commands (hermes hooks list, cron
    ```
  - `scripts/release.py`

    ```diff
    @@ -299,6 +299,7 @@
         "[redacted:email]": "synapsesx",
         "[redacted:email]": "itsflownium",
         "[redacted:email]": "dirtyren",
    +    "[redacted:email]": "bytesnail",  # PR #60360 salvage (--yolo startup ordering; #60328)
         "[redacted:email]": "nullptr0807",  # PR #60956 salvage (gateway hygiene in-place compaction; #60947)
         "[redacted:email]": "embwl0x",  # PR #60810 salvage (channel directory offload)
         "[redacted:email]": "doncazper",
    ```
  - `tests/hermes_cli/test_yolo_startup_order.py`

    ```diff
    @@ -0,0 +1,77 @@
    +"""Regression tests for #60328: --yolo must set HERMES_YOLO_MODE in
    +main() before _prepare_agent_startup() triggers tool imports.
    +
    +The freeze mechanism in tools.approval (_YOLO_MODE_FROZEN) is correct
    +by design (PR #7994). The bug was that main() set the env var inside
    +cmd_chat(), which runs *after* _prepare_agent_startup() has already
    +imported tools.approval and frozen the constant to False.
    +
    +These tests verify the ordering in main() itself: the env var must
    +already be set at the moment _prepare_agent_startup() is called.
    +If someone moves the assignment back into cmd_chat(), these tests
    +fail — catching the exact #60328 regression.
    +"""
    +
    +import os
    +import sys
    +
    +
    +def _run_main_and_capture_yolo_at_startup(monkeypatch, argv):
    +    """Run main() with *argv*, capturing HERMES_YOLO_MODE at the
    +    moment _prepare_agent_startup is called.
    +
    +    Returns the captured env var value (or None if unset).
    +    """
    +    yolo_at_startup = {}
    +
    +    def spy_prepare_startup(args):
    +        yolo_at_startup["value"] = os.environ.get("HERMES_YOLO_MODE")
    +
    +    monkeypatch.setattr(
    +        "hermes_cli.main._prepare_agent_startup", spy_prepare_startup
    +    )
    +    # Stub cmd_chat so main() returns cleanly without entering chat.
    +    monkeypatch.setattr("hermes_cli.main.cmd_chat", lambda args: None)
    +    monkeypatch.delenv("HERMES_YOLO_MODE", raising=False)
    +    monkeypatch.setattr(sys, "argv", argv)
    +
    +    from hermes_cli.main import main as cli_main
    +
    +    cli_main()
    +
    +    return yolo_at_startup.get("value")
    +
    +
    +def test_top_level_yolo_flag_sets_env_before_startup(monkeypatch):
    +    """hermes --yolo must set HERMES_YOLO_MODE before
    +    _prepare_agent_startup imports tools.approval."""
    +    result = _run_main_and_capture_yolo_at_startup(
    +        monkeypatch, ["hermes", "--yolo"]
    +    )
    +    assert result == "1", (
    +        "HERMES_YOLO_MODE was not '1' when _prepare_agent_startup was "
    +        "called from main() with --yolo. This is the #60328 regression: "
    +        "the env var is set too late (inside cmd_chat, after tool imports)."
    +    )
    +
    +
    +def test_chat_subcommand_yolo_flag_sets_env_before_startup(monkeypatch):
    +    """hermes chat --yolo must also set HERMES_YOLO_MODE before
    +    _prepare_agent_startup."""
    +    result = _run_main_and_capture_yolo_at_startup(
    +        monkeypatch, ["hermes", "chat", "--yolo"]
    +    )
    +    assert result == "1", (
    +        "HERMES_YOLO_MODE was not '1' when _prepare_agent_startup was "
    +        "called from main() with 'chat --yolo'."
    +    )
    +
    +
    +def test_no_yolo_flag_leaves_env_unset_at_startup(monkeypatch):
    +    """Without --yolo, HERMES_YOLO_MODE must not be set at startup."""
    +    result = _run_main_and_capture_yolo_at_startup(
    +        monkeypatch, ["hermes"]
    +    )
    +    assert result is None, (
    +        "HERMES_YOLO_MODE was unexpectedly set at startup without --yolo."
    +    )
    ```
- **#34032: fix(cli): use session-scoped yolo bypass in /yolo command**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `5cbc3fbdcc13c3a5d6d0f565a1d1929d0e5e47ff`, `#33931`
- **#60360: fix(cli): set HERMES_YOLO_MODE before plugin discovery at startup**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#61715`, `#60328`
- **#60963: fix(cli): set HERMES_YOLO_MODE before plugin discovery**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#61715`, `#60360`

## Fix status

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