NousResearch/hermes-agent

TUI/dashboard spurious npm reinstall on every launch

Cluster 38772. Data as of

Nothing open on GitHub. Fix merged in #40543 on 7 Jun 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #40543. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
Nothing open on GitHub.

Multiple bugs and fixes around the TUI and dashboard triggering unnecessary npm installs and rebuilds on every launch, primarily because freshness checks compared against the entire monorepo lockfile rather than the scoped workspace closure. Several fixes attempt to narrow the install and audit scope to avoid pulling in unrelated workspace packages.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #40543 fix(install): scope npm installs/audits to avoid pulling in apps/desktop (#38358)

    merged to main

    files: hermes_cli/doctor.py, hermes_cli/main.py, hermes_cli/tools_config.py, package.json, scripts/release.py, tests/hermes_cli/test_cmd_update.py, tests/hermes_cli/test_tui_npm_install.py, tests/hermes_cli/test_web_ui_build.py

    Show the diff
    • hermes_cli/doctor.py
      @@ -1454,18 +1454,29 @@ def run_doctor(args):
           # npm audit for all Node.js packages
           _npm_bin = _safe_which("npm")
           if _npm_bin:
      -        npm_dirs = [
      -            (PROJECT_ROOT, "Browser tools (agent-browser)"),
      -            (PROJECT_ROOT / "scripts" / "whatsapp-bridge", "WhatsApp bridge"),
      +        # Each entry: (cwd, label, extra_audit_args)
      +        # PROJECT_ROOT is audited with --workspaces=false so that the apps/*
      +        # glob (which pulls in Electron, node-pty, etc.) is never resolved
      +        # for a routine security check. The web and ui-tui workspaces are
      +        # audited separately via --workspace flags. See #38772.
      +        npm_audit_targets = [
      +            (PROJECT_ROOT, "Browser tools (agent-browser)", ["--workspaces=false"]),
      +            (PROJECT_ROOT, "web workspace", ["--workspace", "web"]),
      +            (PROJECT_ROOT, "ui-tui workspace", ["--workspace", "ui-tui"]),
      +            (PROJECT_ROOT / "scripts" / "whatsapp-bridge", "WhatsApp bridge", []),
               ]
      -        for npm_dir, label in npm_dirs:
      -            if not (npm_dir / "node_modules").exists():
      +        for npm_dir, label, audit_extra in npm_audit_targets:
      +            # For workspace-scoped audits run from PROJECT_ROOT the
      +            # node_modules check must use the workspace root; standalone dirs
      +            # (whatsapp-bridge) check their own node_modules.
      +            check_dir = PROJECT_ROOT if audit_extra else npm_dir
      +            if not (check_dir / "node_modules").exists():
                       continue
                   try:
                       # Use resolved absolute path so Windows can execute
                       # npm.cmd (CreateProcessW can't run bare .cmd names).
                       audit_result = subprocess.run(
      -                    [_npm_bin, "audit", "--json"],
      +                    [_npm_bin, "audit", "--json", *audit_extra],
                           cwd=str(npm_dir),
                           capture_output=True, text=True, timeout=30,
                       )
      @@ -1476,12 +1487,20 @@ def run_doctor(args):
                       high = vuln_count.get("high", 0)
                       moderate = vuln_count.get("moderate", 0)
                       total = critical + high + moderate
      +                # Determine a scoped fix command for the remediation hint.
      +                if audit_extra and audit_extra[0] == "--workspace":
      +                    fix_scope = " ".join(audit_extra)
      +                    fix_cmd = f"cd {npm_dir} && npm audit fix {fix_scope}"
      +                elif audit_extra == ["--workspaces=false"]:
      +                    fix_cmd = f"cd {npm_dir} && npm audit fix --workspaces=false"
      +                else:
      +                    fix_cmd = f"cd {npm_dir} && npm audit fix"
                       if total == 0:
                           check_ok(f"{label} deps", "(no known vulnerabilities)")
                       elif critical > 0 or high > 0:
                           check_warn(
                               f"{label} deps",
      -                        f"({critical} critical, {high} high, {moderate} moderate — run: cd {npm_dir} && npm audit fix)"
      +                        f"({critical} critical, {high} high, {moderate} moderate — run: {fix_cmd})"
                           )
                           issues.append(
                               f"{label} has {total} npm "
    • hermes_cli/main.py
      @@ -1570,7 +1570,9 @@ def _node_bin(bin: str) -> str:
               if not os.environ.get("HERMES_QUIET"):
                   print("Installing TUI dependencies…")
               npm_cwd = _workspace_root(tui_dir)
      -        npm_workspace_args: tuple[str, ...] = ()
      +        # --workspace ui-tui avoids resolving apps/desktop (Electron + node-pty).
      +        # See #38772.
      +        npm_workspace_args: tuple[str, ...] = ("--workspace", "ui-tui")
               if termux_startup:
                   npm_cwd, npm_workspace_args = _termux_workspace_install_context(
                       tui_dir,
      @@ -7090,7 +7092,11 @@ def _relay(result: "subprocess.CompletedProcess") -> None:
                       _say(text)
       
           npm_cwd = _workspace_root(web_dir)
      -    npm_workspace_args: tuple[str, ...] = ()
      +    # Scope the install to the web workspace only so that the full workspace
      +    # graph (including apps/desktop with its Electron + node-pty deps) is never
      +    # resolved here.  Without --workspace the root package.json's apps/* glob
      +    # would pull in desktop on every web build. See #38772.
      +    npm_workspace_args: tuple[str, ...] = ("--workspace", "web")
           if _is_termux_startup_environment():
               npm_cwd, npm_workspace_args = _termux_workspace_install_context(web_dir)
           r1 = _run_npm_install_deterministic(
      @@ -7105,7 +7111,7 @@ def _relay(result: "subprocess.CompletedProcess") -> None:
               )
               _relay(r1)
               if fatal:
      -            _say("  Run manually:  cd web && npm install && npm run build")
      +            _say("  Run manually:  npm install --workspace web && npm run build -w web")
               return False
           # First attempt — stream output via idle-timeout helper (issue #33788).
           # capture_output=True on a long Vite build looks identical to a hang;
      @@ -7147,7 +7153,7 @@ def _relay(result: "subprocess.CompletedProcess") -> None:
               )
               _relay(r2)
               if fatal:
      -            _say("  Run manually:  cd web && npm install && npm run build")
      +            _say("  Run manually:  npm install --workspace web && npm run build -w web")
               return False
           _say("  ✓ Web UI built")
           return True
      @@ -12373,7 +12379,7 @@ def cmd_dashboard(args):
               )
               if not (_dist_root / "index.html").exists():
                   print(f"✗ --skip-build was passed but no web dist found at: {_dist_root}")
      -            print("  Pre-build first:  cd web && npm install && npm run build")
      +            print("  Pre-build first:  npm install --workspace web && npm run build -w web")
                   print("  Or drop --skip-build to build automatically.")
                   sys.exit(1)
               print(f"→ Skipping web UI build (--skip-build); using dist at {_dist_root}")
    • hermes_cli/tools_config.py
      @@ -846,14 +846,17 @@ def _run_post_setup(post_setup_key: str):
                   # batch shims).  On POSIX npm_bin is the plain path — same
                   # behaviour as before.
                   result = subprocess.run(
      -                [npm_bin, "install", "--silent"],
      +                # --workspaces=false restricts the install to the repo root
      +                # only, avoiding the apps/* glob which would pull in
      +                # apps/desktop (Electron + node-pty) unnecessarily. See #38772.
      +                [npm_bin, "install", "--silent", "--workspaces=false"],
                       capture_output=True, text=True, cwd=str(PROJECT_ROOT)
                   )
                   if result.returncode == 0:
                       _print_success("    Node.js dependencies installed")
                   else:
                       from hermes_constants import display_hermes_home
      -                _print_warning(f"    npm install failed - run manually: cd {display_hermes_home()}/hermes-agent && npm install")
      +                _print_warning(f"    npm install failed - run manually: cd {display_hermes_home()}/hermes-agent && npm install --workspaces=false")
                       if result.stderr:
                           _print_info(f"      {result.stderr.strip()[:200]}")
               elif not node_modules.exists():
      @@ -951,13 +954,14 @@ def _run_post_setup(post_setup_key: str):
                   import subprocess
                   # Absolute npm path so .cmd shim executes on Windows.
                   result = subprocess.run(
      -                [_npm_bin, "install", "--silent"],
      +                # --workspaces=false avoids resolving apps/desktop. See #38772.
      +                [_npm_bin, "install", "--silent", "--workspaces=false"],
                       capture_output=True, text=True, cwd=str(PROJECT_ROOT)
                   )
                   if result.returncode == 0:
                       _print_success("    Camofox installed")
                   else:
      -                _print_warning("    npm install failed - run manually: npm install")
      +                _print_warning("    npm install failed - run manually: npm install --workspaces=false")
               if camofox_dir.exists():
                   _print_info("    Start the Camofox server:")
                   _print_info("      npx @askjo/camofox-browser")
    • package.json
      @@ -10,7 +10,17 @@
           "web"
         ],
         "scripts": {
      -    "postinstall": "echo '✅ Browser tools ready. Run: python run_agent.py --help'"
      +    "postinstall": "echo '✅ Browser tools ready. Run: python run_agent.py --help'",
      +    "install:root": "npm install --workspaces=false",
      +    "install:web": "npm install --workspace web",
      +    "install:tui": "npm install --workspace ui-tui",
      +    "install:desktop": "npm install --workspace apps/desktop",
      +    "audit:root": "npm audit --workspaces=false",
      +    "audit:web": "npm audit --workspace web",
      +    "audit:tui": "npm audit --workspace ui-tui",
      +    "audit:fix:root": "npm audit fix --workspaces=false",
      +    "audit:fix:web": "npm audit fix --workspace web",
      +    "audit:fix:tui": "npm audit fix --workspace ui-tui"
         },
         "repository": {
           "type": "git",
    • scripts/release.py
      @@ -46,6 +46,7 @@
       # Auto-extracted from noreply emails + manual overrides
       AUTHOR_MAP = {
           "[redacted:email]": "randomsnowflake",
      +    "[redacted:email]": "zakame",
           "[redacted:email]": "ViewWay",
           "[redacted:email]": "harjothkhara",
           "[email protected]": "HeLLGURD",
    • tests/hermes_cli/test_cmd_update.py
      @@ -272,7 +272,7 @@ def test_update_refreshes_repo_and_tui_node_dependencies(
                   # The web/ install runs from the workspace root when the root
                   # lockfile exists (npm workspaces hoist node_modules upward).
                   assert npm_calls[2:] == [
      -                (["/usr/bin/npm", "ci", "--silent"], PROJECT_ROOT),
      +                (["/usr/bin/npm", "ci", "--workspace", "web", "--silent"], PROJECT_ROOT),
                   ]
       
               # The web UI build itself went through the streaming helper.
    • tests/hermes_cli/test_tui_npm_install.py
      @@ -255,6 +255,8 @@ def fake_run(*args, **kwargs):
           assert calls[0][0][0] == [
               "/bin/npm",
               "install",
      +        "--workspace",
      +        "ui-tui",
               "--silent",
               "--no-fund",
               "--no-audit",
      @@ -390,3 +392,36 @@ def test_no_stray_lockfiles_in_workspace_subdirs(main_mod) -> None:
               "delete them and run `npm install` from the repo root instead: "
               + ", ".join(str(d / "package-lock.json") for d in stray)
           )
      +
      +
      +def test_tui_launch_install_uses_workspace_scope(
      +    tmp_path: Path, main_mod, monkeypatch
      +) -> None:
      +    """TUI launch npm install must pass --workspace ui-tui to avoid pulling apps/desktop."""
      +    tui_dir = tmp_path / "ui-tui"
      +    tui_dir.mkdir()
      +    (tui_dir / "package.json").write_text("{}")
      +    (tui_dir / "dist" / "entry.js").parent.mkdir(parents=True)
      +    (tui_dir / "dist" / "entry.js").write_text("console.log('tui')")
      +    # workspace root: parent has lockfile, tui_dir does not
      +    (tmp_path / "package-lock.json").write_text("{}")
      +
      +    monkeypatch.setattr(main_mod, "_tui_need_npm_install", lambda _root: True)
      +    monkeypatch.setattr(main_mod, "_tui_need_rebuild", lambda _root: False)
      +    monkeypatch.setattr(main_mod.shutil, "which", lambda name: f"/usr/bin/{name}")
      +
      +    npm_calls = []
      +
      +    def fake_run(cmd, **kwargs):
      +        if cmd[0].endswith("npm"):
      +            npm_calls.append(cmd)
      +        return types.SimpleNamespace(returncode=0, stdout="", stderr="")
      +
      +    monkeypatch.setattr(main_mod.subprocess, "run", fake_run)
      +
      +    main_mod._make_tui_argv(tui_dir, tui_dev=False)
      +
      +    assert npm_calls, "expected npm install to be called"
      +    install_cmd = npm_calls[0]
      +    assert "--workspace" in install_cmd
      +    assert "ui-tui" in install_cmd
    • tests/hermes_cli/test_web_ui_build.py
      @@ -140,6 +140,19 @@ def test_npm_install_uses_utf8_replace_output_decoding(self, tmp_path):
               assert kwargs["encoding"] == "utf-8"
               assert kwargs["errors"] == "replace"
       
      +    def test_npm_install_uses_workspace_web_scope(self, tmp_path):
      +        web_dir, _ = _make_web_dir(tmp_path)
      +        mock_cp = __import__("subprocess").CompletedProcess([], 0, stdout="", stderr="")
      +        build_ok = __import__("subprocess").CompletedProcess([], 0, stdout="", stderr="")
      +        with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \
      +             patch("hermes_cli.main.subprocess.run", return_value=mock_cp) as mock_run, \
      +             patch("hermes_cli.main._run_with_idle_timeout", return_value=build_ok):
      +            result = _build_web_ui(web_dir)
      +        assert result is True
      +        install_cmd = mock_run.call_args[0][0]
      +        assert "--workspace" in install_cmd
      +        assert install_cmd[install_cmd.index("--workspace") + 1] == "web"
      +
           def test_web_build_uses_idle_timeout_helper(self, tmp_path):
               """npm run build now goes through _run_with_idle_timeout (issue #33788).
       
      @@ -205,7 +218,7 @@ def test_desktop_web_install_uses_existing_workspace_root(
       
               assert result is True
               args, kwargs = mock_run.call_args
      -        assert args[0] == ["/usr/bin/npm", "ci", "--silent"]
      +        assert args[0] == ["/usr/bin/npm", "ci", "--workspace", "web", "--silent"]
               assert kwargs["cwd"] == tmp_path
       
       

1 PR closed without merging

From the close comments: merged via another change 1

  • #38810 fix(install): scope npm installs/audits to avoid pulling in apps/desktop · merged via another change → #40543
Closed issues · 1
  • Closed#38772[Bug]: Root package.json workspaces glob unconditionally pulls apps/desktop (and bootstrap-installer) into every unscoped npm command
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Merged PRs
#40543
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
7 Jun 2026

For your agent

The same facts, typed.