# NousResearch/hermes-agent · hermes update command broken across install methods

## Open now

- Nothing open on GitHub.

Multiple bugs and proposed fixes around \`hermes update\` failing under various installation scenarios, including uv tool installs, editable/PyPI installs, system Python, and Debian/Ubuntu PEP 668 restrictions due to incorrect pip or interpreter targeting.

## TL;DR

- 17 member(s): 0 open issue(s), 6 closed issue(s), 0 open PR(s), 3 merged PR(s), 8 PR(s) closed without merging.
- A fix was merged to main in PR #3099. Verify it addresses your case.
- **Recommended:** read PR #3099 (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: `partially_resolved` · deciding PR(s): `pr-3099,pr-35224,pr-35248`. 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.10`: 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

- #2648: Update script uses bare 'pip' instead of venv pip, fails on Debian/Ubuntu with externally-managed-environment error
- #29700: \`hermes update\` fails when Hermes is installed via \`uv tool install\`
- #35031: hermes update fails with 'No virtual environment found' when launcher venv is not active in shell
- #35105: \[Bug\]: \`hermes update\` fails on a PyPI/editable install when \`uv\` is available
- #39444: fix: \`hermes update\` fails with 'No virtual environment found' when using uv
- #48721: \[Bug\]: hermes update on system Python targets wrong interpreter and hits PEP 668 (no-venv + uv branch)

## Prior & competing PRs

- **#3099: fix: use sys.executable for pip in update commands to fix PEP 668**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/main.py`
- **#35224: fix(update): export launcher virtualenv to uv (#35031)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/main.py`, `tests/hermes_cli/test_cmd_update.py`
  - fix hunks:
  - `hermes_cli/main.py`

    ```diff
    @@ -8929,7 +8929,10 @@ def _cmd_update_pip(args):
             cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "hermes-agent"]
     
         print(f"→ Running: {' '.join(cmd)}")
    -    result = subprocess.run(cmd)
    +    run_kwargs = {}
    +    if sys.prefix != sys.base_prefix:
    +        run_kwargs["env"] = {**os.environ, "VIRTUAL_ENV": sys.prefix}
    +    result = subprocess.run(cmd, **run_kwargs)
         if result.returncode != 0:
             print("✗ Update failed")
             sys.exit(1)
    ```
  - `tests/hermes_cli/test_cmd_update.py`

    ```diff
    @@ -39,6 +39,45 @@ def mock_args():
         return SimpleNamespace()
     
     
    +class TestCmdUpdatePip:
    +    """Regression tests for pip-install update flows."""
    +
    +    @patch("shutil.which", return_value="/usr/bin/uv")
    +    @patch("subprocess.run")
    +    def test_update_pip_exports_virtualenv_from_sys_prefix(
    +        self, mock_run, _mock_which, mock_args, monkeypatch
    +    ):
    +        from hermes_cli import main as hm
    +
    +        mock_run.return_value = subprocess.CompletedProcess([], 0, stdout="", stderr="")
    +        monkeypatch.delenv("VIRTUAL_ENV", raising=False)
    +        monkeypatch.setattr(hm.sys, "prefix", "/tmp/hermes-launcher-venv")
    +        monkeypatch.setattr(hm.sys, "base_prefix", "/usr")
    +
    +        hm._cmd_update_pip(mock_args)
    +
    +        assert mock_run.call_count == 1
    +        assert mock_run.call_args.args[0] == ["/usr/bin/uv", "pip", "install", "--upgrade", "hermes-agent"]
    +        assert mock_run.call_args.kwargs["env"]["VIRTUAL_ENV"] == "/tmp/hermes-launcher-venv"
    +
    +    @patch("shutil.which", return_value="/usr/bin/uv")
    +    @patch("subprocess.run")
    +    def test_update_pip_does_not_export_virtualenv_for_system_python(
    +        self, mock_run, _mock_which, mock_args, monkeypatch
    +    ):
    +        from hermes_cli import main as hm
    +
    +        mock_run.return_value = subprocess.CompletedProcess([], 0, stdout="", stderr="")
    +        monkeypatch.delenv("VIRTUAL_ENV", raising=False)
    +        monkeypatch.setattr(hm.sys, "prefix", "/usr")
    +        monkeypatch.setattr(hm.sys, "base_prefix", "/usr")
    +
    +        hm._cmd_update_pip(mock_args)
    +
    +        assert mock_run.call_count == 1
    +        assert "env" not in mock_run.call_args.kwargs
    +
    +
     class TestCmdUpdateBranchFallback:
         """cmd_update falls back to main when current branch has no remote counterpart."""
     
    ```
- **#35248: fix(update): handle uv-tool, pipx, and system-Python installs in hermes update (#29700)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/config.py`, `hermes_cli/main.py`, `scripts/release.py`, `tests/hermes_cli/test_uv_tool_update.py`
  - fix hunks:
  - `hermes_cli/config.py`

    ```diff
    @@ -329,6 +329,34 @@ def stamp_install_method(method: str) -> None:
             pass
     
     
    +def is_uv_tool_install() -> bool:
    +    """Return True when the *running* Hermes lives in a ``uv tool`` layout.
    +
    +    ``uv tool install hermes-agent`` places the install at
    +    ``.../uv/tools/hermes-agent/...`` (default ``~/.local/share/uv/tools``,
    +    or ``$UV_TOOL_DIR/...``). Such installs live outside any virtualenv, so
    +    ``uv pip install`` fails with ``No virtual environment found`` and the
    +    update path must use ``uv tool upgrade`` instead.
    +
    +    Detection is intentionally restricted to properties of the running
    +    interpreter (``sys.prefix`` / ``sys.executable``). We deliberately do
    +    NOT consult ``uv tool list``: it would also return True when
    +    ``hermes-agent`` happens to be uv-tool-installed on the machine while
    +    the *active* Hermes is a regular pip/venv install, causing
    +    ``hermes update`` to upgrade the wrong copy. It would also block on a
    +    subprocess call (~seconds) just to compute a recommendation string.
    +    """
    +    def _has_uv_tool_marker(path: str) -> bool:
    +        norm = os.path.normpath(path).replace(os.sep, "/").lower()
    +        return "/uv/tools/hermes-agent/" in norm + "/"
    +
    +    if _has_uv_tool_marker(sys.prefix):
    +        return True
    +    if _has_uv_tool_marker(sys.executable or ""):
    +        return True
    +    return False
    +
    +
     def recommended_update_command_for_method(method: str) -> str:
         """Return the update command or guidance for a given install method."""
         if method == "nixos":
    ```

    ```diff
    @@ -338,9 +366,10 @@ def recommended_update_command_for_method(method: str) -> str:
         if method == "docker":
             return "docker pull nousresearch/hermes-agent:latest"
         if method == "pip":
    +        if is_uv_tool_install():
    +            return "uv tool upgrade hermes-agent"
             import shutil
    -        uv = shutil.which("uv")
    -        if uv:
    +        if shutil.which("uv"):
                 return "uv pip install --upgrade hermes-agent"
             return "pip install --upgrade hermes-agent"
         return "hermes update"
    ```
  - `hermes_cli/main.py`

    ```diff
    @@ -8971,19 +8971,49 @@ def cmd_update(args):
     def _cmd_update_pip(args):
         """Update Hermes via pip (for PyPI installs)."""
         from hermes_cli import __version__
    +    from hermes_cli.config import is_uv_tool_install
     
         print(f"→ Current version: {__version__}")
         print("→ Checking PyPI for updates...")
     
         uv = shutil.which("uv")
    -    if uv:
    +    in_venv = sys.prefix != sys.base_prefix
    +    # pipx-managed installs live under .../pipx/venvs/<name>/...
    +    pipx_managed = "pipx" in sys.prefix.split(os.sep)
    +    pipx = shutil.which("pipx") if pipx_managed else None
    +
    +    # Only the ``uv pip install`` path inside a venv needs VIRTUAL_ENV
    +    # exported (uv refuses to install without it when the launcher shim
    +    # didn't activate the venv). ``uv tool upgrade`` / ``pipx upgrade``
    +    # operate on a named environment and ignore VIRTUAL_ENV, so we don't
    +    # set it for them.
    +    export_virtualenv = False
    +
    +    if is_uv_tool_install():
    +        if not uv:
    +            print("✗ Detected a uv-tool install but `uv` is not on PATH; install uv and retry.")
    +            sys.exit(1)
    +        cmd = [uv, "tool", "upgrade", "hermes-agent"]
    +    elif pipx_managed and pipx:
    +        # pipx owns its own venv; ``pipx upgrade`` is the only correct path.
    +        # Matches scripts/auto-update.sh, which already uses pipx upgrade.
    +        cmd = [pipx, "upgrade", "hermes-agent"]
    +    elif uv:
             cmd = [uv, "pip", "install", "--upgrade", "hermes-agent"]
    +        if in_venv:
    +            # Launcher shim runs the venv interpreter but doesn't export
    +            # VIRTUAL_ENV; without it uv errors "No virtual environment found".
    +            export_virtualenv = True
    +        else:
    +            # Outside any venv, ``--system`` lets uv target the active
    +            # interpreter, matching pip's default behaviour.
    +            cmd.insert(3, "--system")
         else:
             cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "hermes-agent"]
     
         print(f"→ Running: {' '.join(cmd)}")
         run_kwargs = {}
    -    if sys.prefix != sys.base_prefix:
    +    if export_virtualenv:
             run_kwargs["env"] = {**os.environ, "VIRTUAL_ENV": sys.prefix}
         result = subprocess.run(cmd, **run_kwargs)
         if result.returncode != 0:
    ```
  - `scripts/release.py`

    ```diff
    @@ -651,6 +651,7 @@
         "[redacted:email]": "alexzhu0",
         "[redacted:email]": "AntAISecurityLab",
         "[redacted:email]": "briandevans",
    +    "[redacted:email]": "inchargeautomation-lab",
         "[redacted:email]": "Bartok9",
         "[redacted:email]": "sweetcornna",
         "[redacted:email]": "CruxExperts",
    ```
  - `tests/hermes_cli/test_uv_tool_update.py`

    ```diff
    @@ -0,0 +1,311 @@
    +"""Tests for uv-tool install detection in the update path (issue #29700).
    +
    +``uv tool install hermes-agent`` lives outside any venv, so the previous
    +``uv pip install --upgrade`` update path failed with ``No virtual
    +environment found``. ``is_uv_tool_install`` should detect this layout and
    +both the user-facing recommended command and the actual
    +``_cmd_update_pip`` subprocess invocation should switch to
    +``uv tool upgrade hermes-agent``.
    +
    +Detection is restricted to properties of the running interpreter
    +(``sys.prefix`` / ``sys.executable``) so a pip/venv install on a machine
    +that also has ``uv tool install hermes-agent`` does not get misclassified.
    +"""
    +from __future__ import annotations
    +
    +import subprocess
    +from types import SimpleNamespace
    +from unittest.mock import patch
    +
    +import pytest
    +
    +
    +# ---------------------------------------------------------------------------
    +# is_uv_tool_install
    +# ---------------------------------------------------------------------------
    +
    +
    +class TestIsUvToolInstall:
    +    def test_returns_true_when_sys_prefix_matches_uv_tool_layout(self):
    +        from hermes_cli import config
    +
    +        with patch.object(config.sys, "prefix", "/home/user/.local/share/uv/tools/hermes-agent"):
    +            assert config.is_uv_tool_install() is True
    +
    +    def test_returns_true_when_sys_executable_matches_uv_tool_layout(self):
    +        """Some uv-tool layouts surface the marker on ``sys.executable`` (bin/python)."""
    +        from hermes_cli import config
    +
    +        with patch.object(config.sys, "prefix", "/some/unrelated/venv"), \
    +             patch.object(
    +                 config.sys,
    +                 "executable",
    +                 "/home/user/.local/share/uv/tools/hermes-agent/bin/python",
    +             ):
    +            assert config.is_uv_tool_install() is True
    +
    +    def test_returns_false_when_neither_prefix_nor_executable_matches(self):
    +        from hermes_cli import config
    +
    +        with patch.object(config.sys, "prefix", "/some/unrelated/venv"), \
    +             patch.object(config.sys, "executable", "/usr/bin/python3"):
    +            assert config.is_uv_tool_install() is False
    +
    +    def test_does_not_consult_uv_tool_list(self):
    +        """Detection must NOT shell out: ``uv tool list`` would false-positive
    +        when the active install is pip/venv but the machine also has
    +        ``uv tool install hermes-agent`` somewhere on disk. Copilot review on
    +        PR #29703 flagged this; the fix is to never call ``uv tool list``
    +        from the detection path."""
    +        from hermes_cli import config
    +
    +        with patch.object(config.sys, "prefix", "/some/unrelated/venv"), \
    +             patch.object(config.sys, "executable", "/usr/bin/python3"), \
    +             patch("subprocess.run") as mock_run:
    +            assert config.is_uv_tool_install() is False
    +            mock_run.assert_not_called()
    +
    +    def test_case_insensitive_match(self):
    +        """Match must be case-insensitive — Windows paths preserve case
    +        (e.g. ``...AppData\\Local\\UV\\Tools\\hermes-agent``) and a case-sensitive
    +        check would miss them. We exercise the lower-cased compare path here
    +        without monkey-patching ``os.sep``, which would break the whole suite."""
    +        from hermes_cli import config
    +
    +        with patch.object(
    +            config.sys, "prefix", "/HOME/USER/.local/share/UV/Tools/hermes-agent"
    +        ):
    +            assert config.is_uv_tool_install() is True
    +
    +    def test_handles_empty_executable(self):
    +        from hermes_cli import config
    +
    +        with patch.object(config.sys, "prefix", "/some/unrelated/venv"), \
    +             patch.object(config.sys, "executable", ""):
    +            assert config.is_uv_tool_install() is False
    +
    +
    +# ---------------------------------------------------------------------------
    +# recommended_update_command_for_method
    +# ---------------------------------------------------------------------------
    +
    +
    +class TestRecommendedUpdateCommandForUvTool:
    +    def test_uv_tool_install_recommends_uv_tool_upgrade(self):
    +        from hermes_cli import config
    +
    +        with patch("shutil.which", return_value="/usr/local/bin/uv"), \
    +             patch.object(config, "is_uv_tool_install", return_value=True):
    +            cmd = config.recommended_update_command_for_method("pip")
    +            assert cmd == "uv tool upgrade hermes-agent"
    +
    +    def test_uv_tool_install_recommends_uv_tool_upgrade_even_without_uv_on_path(self):
    +        """Recommendation reflects the *install method*, not whether ``uv`` is
    +        currently on PATH — the user needs to know the right command to run."""
    +        from hermes_cli import config
    +
    +        with patch("shutil.which", return_value=None), \
    +             patch.object(config, "is_uv_tool_install", return_value=True):
    +            cmd = config.recommended_update_command_for_method("pip")
    +            assert cmd == "uv tool upgrade hermes-agent"
    +
    +    def test_uv_pip_install_keeps_legacy_recommendation(self):
    +        """Existing behavior: uv is on PATH but Hermes is a regular pip install."""
    +        from hermes_cli import config
    +
    +        with patch("shutil.which", return_value="/usr/local/bin/uv"), \
    +             patch.object(config, "is_uv_tool_install", return_value=False):
    +            cmd = config.recommended_update_command_for_method("pip")
    +            assert cmd == "uv pip install --upgrade hermes-agent"
    +
    +    def test_no_uv_falls_back_to_plain_pip(self):
    +        from hermes_cli import config
    +
    +        with patch("shutil.which", return_value=None), \
    +             patch.object(config, "is_uv_tool_install", return_value=False):
    +            cmd = config.recommended_update_command_for_method("pip")
    +            assert cmd == "pip install --upgrade hermes-agent"
    +
    +    def test_recommendation_does_not_spawn_subprocess(self):
    +        """Computing the recommendation string must be cheap — no ``uv tool list``
    +        spawn. Copilot review on PR #29703 flagged the prior subprocess hop
    +        as adding overhead and a multi-second timeout window for what is
    +        purely a display string."""
    +        from hermes_cli import config
    +
    +        with patch.object(config.sys, "prefix", "/some/unrelated/venv"), \
    +             patch.object(config.sys, "executable", "/usr/bin/python3"), \
    +             patch("shutil.which", return_value="/usr/local/bin/uv"), \
    +             patch("subprocess.run") as mock_run:
    +            cmd = config.recommended_update_command_for_method("pip")
    +            mock_run.assert_not_called()
    +            assert cmd == "uv pip install --upgrade hermes-agent"
    +
    +
    +# ---------------------------------------------------------------------------
    +# _cmd_update_pip subprocess command
    +# ---------------------------------------------------------------------------
    +
    +
    +class TestCmdUpdatePipUsesUvTool:
    +    @patch("subprocess.run")
    +    def test_runs_uv_tool_upgrade_when_uv_tool_install(self, mock_run):
    +        """The actual subprocess invocation must switch to ``uv tool upgrade``."""
    +        from hermes_cli.main import _cmd_update_pip
    +
    +        mock_run.return_value = subprocess.CompletedProcess(["uv"], 0, stdout="", stderr="")
    +        with patch("shutil.which", return_value="/usr/local/bin/uv"), \
    +             patch("hermes_cli.config.is_uv_tool_install", return_value=True):
    +            _cmd_update_pip(SimpleNamespace())
    +
    +        assert mock_run.call_args[0][0] == ["/usr/local/bin/uv", "tool", "upgrade", "hermes-agent"]
    +
    +    @patch("subprocess.run")
    +    def test_runs_uv_pip_install_when_not_uv_tool(self, mock_run):
    +        """Existing behavior preserved when uv is present but Hermes isn't a tool install."""
    +        from hermes_cli.main import _cmd_update_pip
    +
    +        mock_run.return_value = subprocess.CompletedProcess(["uv"], 0, stdout="", stderr="")
    +        with patch("shutil.which", return_value="/usr/local/bin/uv"), \
    +             patch("hermes_cli.config.is_uv_tool_install", return_value=False):
    +            _cmd_update_pip(SimpleNamespace())
    +
    +        assert mock_run.call_args[0][0] == [
    +            "/usr/local/bin/uv",
    +            "pip",
    +            "install",
    +            "--upgrade",
    +            "hermes-agent",
    +        ]
    +
    +    @patch("subprocess.run")
    +    def test_falls_back_to_pip_when_no_uv(self, mock_run):
    +        from hermes_cli.main import _cmd_update_pip
    +
    +        mock_run.return_value = subprocess.CompletedProcess(["pip"], 0, stdout="", stderr="")
    +        with patch("shutil.which", return_value=None), \
    +             patch("hermes_cli.config.is_uv_tool_install", return_value=False):
    +            _cmd_update_pip(SimpleNamespace())
    +
    +        cmd = mock_run.call_args[0][0]
    +        assert cmd[1:] == ["-m", "pip", "install", "--upgrade", "hermes-agent"]
    +
    +    @patch("subprocess.run")
    +    def test_exits_nonzero_on_subprocess_failure(self, mock_run):
    +        from hermes_cli.main import _cmd_update_pip
    +
    +        mock_run.return_value = subprocess.CompletedProcess(["uv"], 1, stdout="", stderr="")
    +        with patch("shutil.which", return_value="/usr/local/bin/uv"), \
    +             patch("hermes_cli.config.is_uv_tool_install", return_value=True):
    +            with pytest.raises(SystemExit) as exc_info:
    +                _cmd_update_pip(SimpleNamespace())
    +        assert exc_info.value.code == 1
    +
    +    @patch("subprocess.run")
    +    def test_uv_tool_install_without_uv_on_path_exits_with_hint(self, mock_run):
    +        """If the running interpreter looks like a uv-tool install but ``uv`` is
    +        somehow missing from PATH, surface a clear hint instead of silently
    +        falling back to ``python -m pip``, which would either fail (no venv)
    +        or upgrade the wrong copy."""
    +        from hermes_cli.main import _cmd_update_pip
    +
    +        with patch("shutil.which", return_value=None), \
    +             patch("hermes_cli.config.is_uv_tool_install", return_value=True):
    +            with pytest.raises(SystemExit) as exc_info:
    +                _cmd_update_pip(SimpleNamespace())
    +        assert exc_info.value.code == 1
    +        mock_run.assert_not_called()
    +
    +
    +# ---------------------------------------------------------------------------
    +# pipx-managed installs, --system fallback, and VIRTUAL_ENV overlay
    +# (issue #29700 / #35031 family — consolidated update-path handling)
    +# ---------------------------------------------------------------------------
    +
    +
    +class TestCmdUpdatePipInstallLayouts:
    +    """The uv pip path must adapt to where the running interpreter lives:
    +
    +    - inside a venv (launcher shim)  -> export VIRTUAL_ENV, no ``--system``
    +    - bare pip outside any venv      -> add ``--system``, no overlay
    +    - pipx-managed                   -> ``pipx upgrade``
    +    """
    +
    +    @patch("subprocess.run")
    +    def test_pipx_managed_uses_pipx_upgrade(self, mock_run, monkeypatch):
    +        from hermes_cli import main as hm
    +
    +        mock_run.return_value = subprocess.CompletedProcess([], 0, stdout="", stderr="")
    +        monkeypatch.setattr(hm.sys, "prefix", "/home/u/.local/pipx/venvs/hermes-agent")
    +        monkeypatch.setattr(hm.sys, "base_prefix", "/usr")
    +
    +        def _which(name):
    +            return {"uv": "/usr/bin/uv", "pipx": "/usr/bin/pipx"}.get(name)
    +
    +        with patch("shutil.which", side_effect=_which), \
    +             patch("hermes_cli.config.is_uv_tool_install", return_value=False):
    +            hm._cmd_update_pip(SimpleNamespace())
    +
    +        assert mock_run.call_args[0][0] == ["/usr/bin/pipx", "upgrade", "hermes-agent"]
    +        # pipx upgrade ignores VIRTUAL_ENV; we must not set it.
    +        assert "env" not in mock_run.call_args.kwargs
    +
    +    @patch("subprocess.run")
    +    def test_pipx_layout_without_pipx_binary_treated_as_venv(
    +        self, mock_run, monkeypatch
    +    ):
    +        from hermes_cli import main as hm
    +
    +        mock_run.return_value = subprocess.CompletedProcess([], 0, stdout="", stderr="")
    +        monkeypatch.setattr(hm.sys, "prefix", "/home/u/.local/pipx/venvs/hermes-agent")
    +        monkeypatch.setattr(hm.sys, "base_prefix", "/usr")
    +
    +        # pipx layout detected via prefix, but pipx binary missing on PATH.
    +        def _which(name):
    +            return "/usr/bin/uv" if name == "uv" else None
    +
    +        with patch("shutil.which", side_effect=_which), \
    +             patch("hermes_cli.config.is_uv_tool_install", return_value=False):
    +            hm._cmd_update_pip(SimpleNamespace())
    +
    +        # prefix != base_prefix, so this is treated as a venv -> overlay, no --system.
    +        assert mock_run.call_args[0][0] == [
    +            "/usr/bin/uv", "pip", "install", "--upgrade", "hermes-agent",
    +        ]
    +        assert mock_run.call_args.kwargs["env"]["VIRTUAL_ENV"].endswith("hermes-agent")
    +
    +    @patch("subprocess.run")
    +    def test_bare_pip_outside_venv_adds_system(self, mock_run, monkeypatch):
    +        from hermes_cli import main as hm
    +
    +        mock_run.return_value = subprocess.CompletedProcess([], 0, stdout="", stderr="")
    +        # No venv: prefix == base_prefix.
    +        monkeypatch.setattr(hm.sys, "prefix", "/usr")
    +        monkeypatch.setattr(hm.sys, "base_prefix", "/usr")
    +
    +        with patch("shutil.which", return_value="/usr/bin/uv"), \
    +             patch("hermes_cli.config.is_uv_tool_install", return_value=False):
    +            hm._cmd_update_pip(SimpleNamespace())
    +
    +        assert mock_run.call_args[0][0] == [
    +            "/usr/bin/uv", "pip", "install", "--system", "--upgrade", "hermes-agent",
    +        ]
    +        assert "env" not in mock_run.call_args.kwargs
    +
    +    @patch("subprocess.run")
    +    def test_venv_exports_virtualenv_and_omits_system(self, mock_run, monkeypatch):
    +        from hermes_cli import main as hm
    +
    +        mock_run.return_value = subprocess.CompletedProcess([], 0, stdout="", stderr="")
    +        monkeypatch.delenv("VIRTUAL_ENV", raising=False)
    +        monkeypatch.setattr(hm.sys, "prefix", "/home/u/.hermes/hermes-agent/venv")
    +        monkeypatch.setattr(hm.sys, "base_prefix", "/usr")
    +
    +        with patch("shutil.which", return_value="/usr/bin/uv"), \
    +             patch("hermes_cli.config.is_uv_tool_install", return_value=False):
    +            hm._cmd_update_pip(SimpleNamespace())
    +
    +        cmd = mock_run.call_args[0][0]
    +        assert "--system" not in cmd
    +        assert cmd == ["/usr/bin/uv", "pip", "install", "--upgrade", "hermes-agent"]
    +        assert mock_run.call_args.kwargs["env"]["VIRTUAL_ENV"] == "/home/u/.hermes/hermes-agent/venv"
    ```
- **#48726: fix(cli): pin --python and add --break-system-packages for no-venv uv update**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#48721`, `#48721`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#2655: fix: use sys.executable for pip in update script to fix PEP 668 (#2648)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`
- **#2715: fix(cli): use venv pip in update command instead of system pip**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#3099`, `#2655`
- **#29701: fix(cli): add --system flag to uv pip install for uv tool installs (#29700)**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#29703: fix(cli): use \`uv tool upgrade\` when Hermes is a uv tool install (#29700)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#35248`
- **#29707: fix(cli): use 'uv tool upgrade' when hermes was installed via 'uv tool install' (#29700)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#35248`
- **#35064: fix(update): export launcher virtualenv to uv (#35031)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#35224`
- **#48756: fix(cli): target correct Python in no-venv uv update path (#48721)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#48726`, `#48721`

## Fix status

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