# NousResearch/hermes-agent · pip bootstrap and dependency install fixes in setup

## Open now

- Nothing open on GitHub.

These issues and PRs address failures to install dependencies (notably NeuTTS) during setup, focusing on reliably locating or bootstrapping pip in virtual environments, particularly on newer Ubuntu systems and PEP 668-constrained environments.

## TL;DR

- 4 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #58523. Verify it addresses your case.
- **Recommended:** read PR #58523 (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.50`: 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

- #3002: \[Bug\]: Fails to install NeuTTS during setup

## Prior & competing PRs

- **#58523: fix(setup): install deps via canonical uv→pip→ensurepip ladder (salvage #3019)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `agent/lsp/install.py`, `hermes_cli/dingtalk_auth.py`, `hermes_cli/memory_setup.py`, `hermes_cli/setup.py`, `plugins/google_meet/cli.py`, `plugins/memory/honcho/cli.py`, `plugins/platforms/google_chat/oauth.py`, `plugins/platforms/matrix/adapter.py`, `tests/hermes_cli/test_memory_setup_provider_arg.py`, `tests/plugins/test_google_meet_plugin.py`
  - fix hunks:
  - `agent/lsp/install.py`

    ```diff
    @@ -348,17 +348,15 @@ def _install_pip(pkg: str, bin_name: str) -> Optional[str]:
         pip_target.mkdir(parents=True, exist_ok=True)
         try:
             logger.info("[install] pip install --target %s %s", pip_target, pkg)
    -        proc = subprocess.run(
    -            [sys.executable, "-m", "pip", "install", "--target", str(pip_target), "--quiet", pkg],
    -            check=False,
    -            capture_output=True,
    -            text=True,
    +        from hermes_cli.tools_config import _pip_install
    +
    +        proc = _pip_install(
    +            ["--target", str(pip_target), "--quiet", pkg],
                 timeout=300,
    -            stdin=subprocess.DEVNULL,
             )
             if proc.returncode != 0:
                 logger.warning(
    -                "[install] pip install failed for %s: %s", pkg, proc.stderr.strip()[:500]
    +                "[install] pip install failed for %s: %s", pkg, (proc.stderr or "").strip()[:500]
                 )
                 return None
         except (subprocess.TimeoutExpired, OSError) as e:
    ```
  - `hermes_cli/dingtalk_auth.py`

    ```diff
    @@ -163,17 +163,15 @@ def _ensure_qrcode_installed() -> bool:
     
         import subprocess
     
    -    # Try uv first (Hermes convention), then pip
    -    for cmd in (
    -        [sys.executable, "-m", "uv", "pip", "install", "qrcode"],
    -        [sys.executable, "-m", "pip", "install", "-q", "qrcode"],
    -    ):
    -        try:
    -            subprocess.check_call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    +    from hermes_cli.tools_config import _pip_install
    +
    +    try:
    +        result = _pip_install(["-q", "qrcode"], timeout=120)
    +        if result.returncode == 0:
                 import qrcode  # noqa: F401,F811
                 return True
    -        except (subprocess.CalledProcessError, ImportError, FileNotFoundError):
    -            continue
    +    except (subprocess.SubprocessError, ImportError, OSError):
    +        pass
         return False
     
     
    ```
  - `hermes_cli/memory_setup.py`

    ```diff
    @@ -122,36 +122,19 @@ def _install_dependencies(provider_name: str) -> None:
     
         print(f"\n  Installing dependencies: {', '.join(missing)}")
     
    -    import shutil
    -
    -    uv_path = shutil.which("uv")
    -    if uv_path:
    -        install_cmd = [uv_path, "pip", "install", "--python", sys.executable, "--quiet"] + missing
    -        manual_cmd = f"uv pip install --python {sys.executable} {' '.join(missing)}"
    -    else:
    -        pip_cmd = shutil.which("pip3") or shutil.which("pip")
    -        if not pip_cmd:
    -            print("  ⚠ uv not found — cannot install dependencies")
    -            print("  Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh")
    -            print("  Then re-run: hermes memory setup")
    -            return
    -        print("  ⚠ uv not found. Falling back to standard pip...")
    -        install_cmd = [sys.executable, "-m", "pip", "install", "--quiet"] + missing
    -        manual_cmd = f"{sys.executable} -m pip install {' '.join(missing)}"
    +    from hermes_cli.tools_config import _pip_install
     
    +    manual_cmd = f"uv pip install {' '.join(missing)}"
         try:
    -        subprocess.run(
    -            install_cmd,
    -            check=True, timeout=120,
    -            capture_output=True,
    -        )
    -        print(f"  ✓ Installed {', '.join(missing)}")
    -    except subprocess.CalledProcessError as e:
    -        print(f"  ⚠ Failed to install {', '.join(missing)}")
    -        stderr = (e.stderr or b"").decode()[:200]
    -        if stderr:
    -            print(f"    {stderr}")
    -        print(f"  Run manually: {manual_cmd}")
    +        result = _pip_install(["--quiet"] + missing, timeout=120)
    +        if result.returncode == 0:
    +            print(f"  ✓ Installed {', '.join(missing)}")
    +        else:
    +            print(f"  ⚠ Failed to install {', '.join(missing)}")
    +            stderr = (result.stderr or "")[:200]
    +            if stderr:
    +                print(f"    {stderr}")
    +            print(f"  Run manually: {manual_cmd}")
         except Exception as e:
             print(f"  ⚠ Install failed: {e}")
             print(f"  Run manually: {manual_cmd}")
    ```
  - `hermes_cli/setup.py`

    ```diff
    @@ -828,17 +828,24 @@ def _install_neutts_deps() -> bool:
         print_info("Installing neutts Python package...")
         print_info("This will also download the TTS model (~300MB) on first use.")
         print()
    +
    +    # Route through the canonical uv → pip → ensurepip ladder so pip-less
    +    # venvs (Ubuntu 25.10 `python -m venv`, `uv venv`) work out of the box.
    +    from hermes_cli.tools_config import _pip_install
    +
         try:
    -        subprocess.run(
    -            [sys.executable, "-m", "pip", "install", "-U", "neutts[all]", "--quiet"],
    -            check=True, timeout=300,
    -        )
    -        print_success("neutts installed successfully")
    -        return True
    -    except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
    +        result = _pip_install(["-U", "neutts[all]", "--quiet"], timeout=300)
    +    except Exception as e:
             print_error(f"Failed to install neutts: {e}")
    -        print_info("Try manually: python -m pip install -U neutts[all]")
    +        print_info("Try manually: uv pip install -U 'neutts[all]'")
             return False
    +    if result.returncode == 0:
    +        print_success("neutts installed successfully")
    +        return True
    +    err = (result.stderr or "").strip()
    +    print_error(f"Failed to install neutts: {err[:300] if err else 'install failed'}")
    +    print_info("Try manually: uv pip install -U 'neutts[all]'")
    +    return False
     
     
     def _install_kittentts_deps() -> bool:
    ```

    ```diff
    @@ -853,17 +860,22 @@ def _install_kittentts_deps() -> bool:
         print()
         print_info("Installing kittentts Python package (~25-80MB model downloaded on first use)...")
         print()
    +
    +    from hermes_cli.tools_config import _pip_install
    +
         try:
    -        subprocess.run(
    -            [sys.executable, "-m", "pip", "install", "-U", wheel_url, "soundfile", "--quiet"],
    -            check=True, timeout=300,
    -        )
    -        print_success("kittentts installed successfully")
    -        return True
    -    except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
    +        result = _pip_install(["-U", wheel_url, "soundfile", "--quiet"], timeout=300)
    +    except Exception as e:
             print_error(f"Failed to install kittentts: {e}")
    -        print_info(f"Try manually: python -m pip install -U '{wheel_url}' soundfile")
    +        print_info(f"Try manually: uv pip install -U '{wheel_url}' soundfile")
             return False
    +    if result.returncode == 0:
    +        print_success("kittentts installed successfully")
    +        return True
    +    err = (result.stderr or "").strip()
    +    print_error(f"Failed to install kittentts: {err[:300] if err else 'install failed'}")
    +    print_info(f"Try manually: uv pip install -U '{wheel_url}' soundfile")
    +    return False
     
     
     def _xai_oauth_logged_in_for_setup() -> bool:
    ```

    ```diff
    @@ -1302,32 +1314,13 @@ def setup_terminal_backend(config: dict):
                     __import__("modal")
                 except ImportError:
                     print_info("Installing modal SDK...")
    -                import subprocess
    -
    -                uv_bin = shutil.which("uv")
    -                if uv_bin:
    -                    result = subprocess.run(
    -                        [
    -                            uv_bin,
    -                            "pip",
    -                            "install",
    -                            "--python",
    -                            sys.executable,
    -                            "modal",
    -                        ],
    -                        capture_output=True,
    -                        text=True,
    -                    )
    -                else:
    -                    result = subprocess.run(
    -                        [sys.executable, "-m", "pip", "install", "modal"],
    -                        capture_output=True,
    -                        text=True,
    -                    )
    +                from hermes_cli.tools_config import _pip_install
    +
    +                result = _pip_install(["modal"])
                     if result.returncode == 0:
                         print_success("modal SDK installed")
                     else:
    -                    print_warning("Install failed — run manually: pip install modal")
    +                    print_warning("Install failed — run manually: uv pip install modal")
     
                 # Modal token
                 print()
    ```

    ```diff
    @@ -1362,25 +1355,13 @@ def setup_terminal_backend(config: dict):
                 __import__("daytona")
             except ImportError:
                 print_info("Installing daytona SDK...")
    -            import subprocess
    +            from hermes_cli.tools_config import _pip_install
     
    -            uv_bin = shutil.which("uv")
    -            if uv_bin:
    -                result = subprocess.run(
    -                    [uv_bin, "pip", "install", "--python", sys.executable, "daytona"],
    -                    capture_output=True,
    -                    text=True,
    -                )
    -            else:
    -                result = subprocess.run(
    -                    [sys.executable, "-m", "pip", "install", "daytona"],
    -                    capture_output=True,
    -                    text=True,
    -                )
    +            result = _pip_install(["daytona"])
                 if result.returncode == 0:
                     print_success("daytona SDK installed")
                 else:
    -                print_warning("Install failed — run manually: pip install daytona")
    +                print_warning("Install failed — run manually: uv pip install daytona")
                     if result.stderr:
                         print_info(f"  Error: {result.stderr.strip().splitlines()[-1]}")
     
    ```
  - `plugins/google_meet/cli.py`

    ```diff
    @@ -250,10 +250,9 @@ def _confirm(prompt: str) -> bool:
         pip_pkgs = ["playwright", "websockets"]
         print(f"\n[1/3] pip install: {' '.join(pip_pkgs)}")
         try:
    -        res = _sp.run(
    -            [sys.executable, "-m", "pip", "install", "--upgrade", *pip_pkgs],
    -            check=False,
    -        )
    +        from hermes_cli.tools_config import _pip_install
    +
    +        res = _pip_install(["--upgrade", *pip_pkgs], capture_output=False)
             if res.returncode != 0:
                 print("  pip install failed")
                 return 1
    ```
  - `plugins/memory/honcho/cli.py`

    ```diff
    @@ -515,20 +515,16 @@ def _ensure_sdk_installed() -> bool:
             print("  Skipping install. Run: pip install 'honcho-ai>=2.0.1'\n")
             return False
     
    -    import subprocess
         print("  Installing honcho-ai...", flush=True)
    -    result = subprocess.run(
    -        [sys.executable, "-m", "pip", "install", "honcho-ai>=2.0.1"],
    -        capture_output=True,
    -        text=True,
    -        stdin=subprocess.DEVNULL,
    -    )
    +    from hermes_cli.tools_config import _pip_install
    +
    +    result = _pip_install(["honcho-ai>=2.0.1"])
         if result.returncode == 0:
             print("  Installed.\n")
             return True
         else:
    -        print(f"  Install failed:\n{result.stderr.strip()}")
    -        print("  Run manually: pip install 'honcho-ai>=2.0.1'\n")
    +        print(f"  Install failed:\n{(result.stderr or '').strip()}")
    +        print("  Run manually: uv pip install 'honcho-ai>=2.0.1'\n")
             return False
     
     
    ```
  - `plugins/platforms/google_chat/oauth.py`

    ```diff
    @@ -379,13 +379,14 @@ def install_deps() -> bool:
     
         print("Installing Google Chat OAuth dependencies...")
         try:
    -        subprocess.check_call(
    -            [sys.executable, "-m", "pip", "install", "--quiet"] + _REQUIRED_PACKAGES,
    -            stdout=subprocess.DEVNULL,
    -        )
    +        from hermes_cli.tools_config import _pip_install
    +
    +        result = _pip_install(["--quiet"] + _REQUIRED_PACKAGES)
    +        if result.returncode != 0:
    +            raise RuntimeError((result.stderr or "install failed").strip()[:300])
             print("Dependencies installed.")
             return True
    -    except subprocess.CalledProcessError as exc:
    +    except Exception as exc:
             print(f"ERROR: Failed to install dependencies: {exc}")
             print("Or install via the optional extra:")
             print("  pip install 'hermes-agent[google_chat]'")
    ```
  - `plugins/platforms/matrix/adapter.py`

    ```diff
    @@ -4497,23 +4497,14 @@ def interactive_setup() -> None:
                     __import__("mautrix")
                 except ImportError:
                     print_info(f"Installing {matrix_pkg}...")
    -                import subprocess
    -                uv_bin = shutil.which("uv")
    -                if uv_bin:
    -                    result = subprocess.run(
    -                        [uv_bin, "pip", "install", "--python", _sys.executable, matrix_pkg],
    -                        capture_output=True, text=True,
    -                    )
    -                else:
    -                    result = subprocess.run(
    -                        [_sys.executable, "-m", "pip", "install", matrix_pkg],
    -                        capture_output=True, text=True,
    -                    )
    +                from hermes_cli.tools_config import _pip_install
    +
    +                result = _pip_install([matrix_pkg])
                     if result.returncode == 0:
                         print_success(f"{matrix_pkg} installed")
                     else:
                         print_warning(
    -                        f"Install failed — run manually: pip install "
    +                        f"Install failed — run manually: uv pip install "
                             f"'{matrix_pkg}' asyncpg aiosqlite Markdown aiohttp-socks"
                         )
     
    ```
  - `tests/hermes_cli/test_memory_setup_provider_arg.py`

    ```diff
    @@ -51,47 +51,55 @@ def test_unknown_provider_reports_and_returns_early(self, capsys):
     
     
     class TestInstallDependenciesRunner:
    -    """`_install_dependencies` must install via `uv` when present and fall back
    -    to standard `pip` when `uv` is unavailable (e.g. slim containers / CI images
    -    that don't ship uv) instead of dead-ending with "cannot install"."""
    +    """`_install_dependencies` must route through the canonical
    +    ``_pip_install`` ladder (uv → pip → ensurepip): uv when present, standard
    +    pip when uv is unavailable, and an ensurepip bootstrap for pip-less venvs
    +    instead of dead-ending with "cannot install"."""
     
    -    def _run_with_missing_dep(self, tmp_path, which_side_effect):
    +    def _run_with_missing_dep(self, tmp_path, which_side_effect, run_behavior=None):
             """Drive _install_dependencies for a plugin that declares one missing
    -        pip dep, capturing the subprocess.run argv (or None if never called)."""
    +        pip dep, capturing every subprocess.run argv issued by the ladder."""
             import sys
     
             (tmp_path / "plugin.yaml").write_text(
                 "pip_dependencies:\n  - definitely-not-installed-xyz\n", encoding="utf-8"
             )
    -        captured = {}
    +        calls = []
     
             def fake_run(cmd, **kw):
    -            captured["cmd"] = cmd
    -            return SimpleNamespace()
    +            calls.append(cmd)
    +            if run_behavior:
    +                return run_behavior(cmd)
    +            return SimpleNamespace(returncode=0, stdout="", stderr="")
     
             with patch("plugins.memory.find_provider_dir", return_value=tmp_path), \
    -             patch("shutil.which", side_effect=which_side_effect), \
    -             patch("subprocess.run", fake_run):
    +             patch("hermes_cli.tools_config.shutil.which", side_effect=which_side_effect), \
    +             patch("hermes_cli.tools_config.subprocess.run", fake_run):
                 memory_setup._install_dependencies("x")
    -        return captured.get("cmd"), sys.executable
    +        return calls, sys.executable
     
         def test_uses_uv_when_available(self, tmp_path):
    -        cmd, _ = self._run_with_missing_dep(
    +        calls, _ = self._run_with_missing_dep(
                 tmp_path, lambda b: "/usr/bin/uv" if b == "uv" else None
             )
    -        assert cmd is not None
    -        assert cmd[:3] == ["/usr/bin/uv", "pip", "install"]
    -
    -    def test_falls_back_to_pip_when_uv_missing(self, tmp_path, capsys):
    -        """The salvaged behavior (#5954): no uv but pip present -> python -m pip."""
    -        cmd, py = self._run_with_missing_dep(
    -            tmp_path, lambda b: "/usr/bin/pip3" if b == "pip3" else None
    -        )
    -        assert cmd is not None
    -        assert cmd[:4] == [py, "-m", "pip", "install"]
    -        assert "Falling back to standard pip" in capsys.readouterr().out
    -
    -    def test_aborts_when_neither_uv_nor_pip(self, tmp_path, capsys):
    -        cmd, _ = self._run_with_missing_dep(tmp_path, lambda b: None)
    -        assert cmd is None  # no install attempted
    -        assert "cannot install dependencies" in capsys.readouterr().out
    +        assert calls
    +        assert calls[0][:3] == ["/usr/bin/uv", "pip", "install"]
    +
    +    def test_falls_back_to_pip_when_uv_missing(self, tmp_path):
    +        """No uv but pip importable -> python -m pip install."""
    +        calls, py = self._run_with_missing_dep(tmp_path, lambda b: None)
    +        assert calls
    +        # Ladder probes pip first, then installs with it.
    +        assert calls[0][:3] == [py, "-m", "pip"]
    +        assert calls[-1][:4] == [py, "-m", "pip", "install"]
    +
    +    def test_bootstraps_pip_via_ensurepip_when_missing(self, tmp_path):
    +        """Neither uv nor pip -> ensurepip bootstrap, then pip install."""
    +        def behavior(cmd):
    +            if cmd[-1] == "--version":
    +                return SimpleNamespace(returncode=1, stdout="", stderr="")
    +            return SimpleNamespace(returncode=0, stdout="", stderr="")
    +
    +        calls, py = self._run_with_missing_dep(tmp_path, lambda b: None, behavior)
    +        assert any("ensurepip" in c for c in calls)
    +        assert calls[-1][:4] == [py, "-m", "pip", "install"]
    ```
  - `tests/plugins/test_google_meet_plugin.py`

    ```diff
    @@ -778,11 +778,13 @@ def _fake_run(argv, **kwargs):
              patch("shutil.which", return_value="/usr/bin/paplay"):
             rc = _cmd_install(realtime=False, assume_yes=True)
         assert rc == 0
    -    # First invocation: pip install
    -    pip_cmds = [c for c in calls if len(c) > 2 and c[1:4] == ["-m", "pip", "install"]]
    -    assert pip_cmds, f"no pip install run: {calls}"
    -    assert "playwright" in pip_cmds[0]
    -    assert "websockets" in pip_cmds[0]
    +    # First invocation: dependency install via the uv→pip ladder
    +    # (shutil.which is mocked truthy, so the uv tier is taken: `<uv> pip install ...`)
    +    pip_cmds = [
    +        c for c in calls
    +        if "install" in c and "playwright" in c and "websockets" in c
    +    ]
    +    assert pip_cmds, f"no dependency install run: {calls}"
         # Second: playwright install chromium
         pw_cmds = [c for c in calls if len(c) > 2 and c[1:4] == ["-m", "playwright", "install"]]
         assert pw_cmds, f"no playwright install run: {calls}"
    ```
- **#3019: fix(setup): bootstrap pip with ensurepip when not available in venv before neutts install**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `569b78c1f`, `#58523`
- **#8384: fix(install): bootstrap pip when missing from venv on Ubuntu 25.10+**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#3002`, `#58523`, `#3019`

## Fix status

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