{"repo":"NousResearch/hermes-agent","public_id":"38358","display_label":"Scope npm install to web workspace only","summary":"A bug where npm install commands lacked the --workspace web flag caused issues in multi-workspace repositories. The fix scopes npm install and audit commands to the correct workspace to avoid unintended side effects.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":40543,"merged_at":"2026-06-07T01:22:21+00:00"},"resolution":{"state":"resolved","by":"pr-40543"},"freshness":{"state":"verified","last_verified_at":"2026-06-07T01:22:21+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":38358,"pr_number":40543,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-07T01:22:21+00:00"},{"issue_number":38772,"pr_number":40543,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-07T01:22:21+00:00"}],"members":[{"kind":"issue","number":38358,"title":"_build_web_ui() npm install missing --workspace web flag breaks \"hermes update\" on multi-workspace repos","url":null,"state":"closed"},{"kind":"pr","number":38396,"title":"fix(cli): scope web UI npm install to --workspace web","url":null,"state":"closed","close_reason":{"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":40543,"title":"fix(install): scope npm installs/audits to avoid pulling in apps/desktop (#38358)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["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"],"fix_hunks":[{"file":"hermes_cli/doctor.py","hunks":["@@ -1454,18 +1454,29 @@ def run_doctor(args):\n     # npm audit for all Node.js packages\n     _npm_bin = _safe_which(\"npm\")\n     if _npm_bin:\n-        npm_dirs = [\n-            (PROJECT_ROOT, \"Browser tools (agent-browser)\"),\n-            (PROJECT_ROOT / \"scripts\" / \"whatsapp-bridge\", \"WhatsApp bridge\"),\n+        # Each entry: (cwd, label, extra_audit_args)\n+        # PROJECT_ROOT is audited with --workspaces=false so that the apps/*\n+        # glob (which pulls in Electron, node-pty, etc.) is never resolved\n+        # for a routine security check. The web and ui-tui workspaces are\n+        # audited separately via --workspace flags. See #38772.\n+        npm_audit_targets = [\n+            (PROJECT_ROOT, \"Browser tools (agent-browser)\", [\"--workspaces=false\"]),\n+            (PROJECT_ROOT, \"web workspace\", [\"--workspace\", \"web\"]),\n+            (PROJECT_ROOT, \"ui-tui workspace\", [\"--workspace\", \"ui-tui\"]),\n+            (PROJECT_ROOT / \"scripts\" / \"whatsapp-bridge\", \"WhatsApp bridge\", []),\n         ]\n-        for npm_dir, label in npm_dirs:\n-            if not (npm_dir / \"node_modules\").exists():\n+        for npm_dir, label, audit_extra in npm_audit_targets:\n+            # For workspace-scoped audits run from PROJECT_ROOT the\n+            # node_modules check must use the workspace root; standalone dirs\n+            # (whatsapp-bridge) check their own node_modules.\n+            check_dir = PROJECT_ROOT if audit_extra else npm_dir\n+            if not (check_dir / \"node_modules\").exists():\n                 continue\n             try:\n                 # Use resolved absolute path so Windows can execute\n                 # npm.cmd (CreateProcessW can't run bare .cmd names).\n                 audit_result = subprocess.run(\n-                    [_npm_bin, \"audit\", \"--json\"],\n+                    [_npm_bin, \"audit\", \"--json\", *audit_extra],\n                     cwd=str(npm_dir),\n                     capture_output=True, text=True, timeout=30,\n                 )","@@ -1476,12 +1487,20 @@ def run_doctor(args):\n                 high = vuln_count.get(\"high\", 0)\n                 moderate = vuln_count.get(\"moderate\", 0)\n                 total = critical + high + moderate\n+                # Determine a scoped fix command for the remediation hint.\n+                if audit_extra and audit_extra[0] == \"--workspace\":\n+                    fix_scope = \" \".join(audit_extra)\n+                    fix_cmd = f\"cd {npm_dir} && npm audit fix {fix_scope}\"\n+                elif audit_extra == [\"--workspaces=false\"]:\n+                    fix_cmd = f\"cd {npm_dir} && npm audit fix --workspaces=false\"\n+                else:\n+                    fix_cmd = f\"cd {npm_dir} && npm audit fix\"\n                 if total == 0:\n                     check_ok(f\"{label} deps\", \"(no known vulnerabilities)\")\n                 elif critical > 0 or high > 0:\n                     check_warn(\n                         f\"{label} deps\",\n-                        f\"({critical} critical, {high} high, {moderate} moderate — run: cd {npm_dir} && npm audit fix)\"\n+                        f\"({critical} critical, {high} high, {moderate} moderate — run: {fix_cmd})\"\n                     )\n                     issues.append(\n                         f\"{label} has {total} npm \""]},{"file":"hermes_cli/main.py","hunks":["@@ -1570,7 +1570,9 @@ def _node_bin(bin: str) -> str:\n         if not os.environ.get(\"HERMES_QUIET\"):\n             print(\"Installing TUI dependencies…\")\n         npm_cwd = _workspace_root(tui_dir)\n-        npm_workspace_args: tuple[str, ...] = ()\n+        # --workspace ui-tui avoids resolving apps/desktop (Electron + node-pty).\n+        # See #38772.\n+        npm_workspace_args: tuple[str, ...] = (\"--workspace\", \"ui-tui\")\n         if termux_startup:\n             npm_cwd, npm_workspace_args = _termux_workspace_install_context(\n                 tui_dir,","@@ -7090,7 +7092,11 @@ def _relay(result: \"subprocess.CompletedProcess\") -> None:\n                 _say(text)\n \n     npm_cwd = _workspace_root(web_dir)\n-    npm_workspace_args: tuple[str, ...] = ()\n+    # Scope the install to the web workspace only so that the full workspace\n+    # graph (including apps/desktop with its Electron + node-pty deps) is never\n+    # resolved here.  Without --workspace the root package.json's apps/* glob\n+    # would pull in desktop on every web build. See #38772.\n+    npm_workspace_args: tuple[str, ...] = (\"--workspace\", \"web\")\n     if _is_termux_startup_environment():\n         npm_cwd, npm_workspace_args = _termux_workspace_install_context(web_dir)\n     r1 = _run_npm_install_deterministic(","@@ -7105,7 +7111,7 @@ def _relay(result: \"subprocess.CompletedProcess\") -> None:\n         )\n         _relay(r1)\n         if fatal:\n-            _say(\"  Run manually:  cd web && npm install && npm run build\")\n+            _say(\"  Run manually:  npm install --workspace web && npm run build -w web\")\n         return False\n     # First attempt — stream output via idle-timeout helper (issue #33788).\n     # capture_output=True on a long Vite build looks identical to a hang;","@@ -7147,7 +7153,7 @@ def _relay(result: \"subprocess.CompletedProcess\") -> None:\n         )\n         _relay(r2)\n         if fatal:\n-            _say(\"  Run manually:  cd web && npm install && npm run build\")\n+            _say(\"  Run manually:  npm install --workspace web && npm run build -w web\")\n         return False\n     _say(\"  ✓ Web UI built\")\n     return True","@@ -12373,7 +12379,7 @@ def cmd_dashboard(args):\n         )\n         if not (_dist_root / \"index.html\").exists():\n             print(f\"✗ --skip-build was passed but no web dist found at: {_dist_root}\")\n-            print(\"  Pre-build first:  cd web && npm install && npm run build\")\n+            print(\"  Pre-build first:  npm install --workspace web && npm run build -w web\")\n             print(\"  Or drop --skip-build to build automatically.\")\n             sys.exit(1)\n         print(f\"→ Skipping web UI build (--skip-build); using dist at {_dist_root}\")"]},{"file":"hermes_cli/tools_config.py","hunks":["@@ -846,14 +846,17 @@ def _run_post_setup(post_setup_key: str):\n             # batch shims).  On POSIX npm_bin is the plain path — same\n             # behaviour as before.\n             result = subprocess.run(\n-                [npm_bin, \"install\", \"--silent\"],\n+                # --workspaces=false restricts the install to the repo root\n+                # only, avoiding the apps/* glob which would pull in\n+                # apps/desktop (Electron + node-pty) unnecessarily. See #38772.\n+                [npm_bin, \"install\", \"--silent\", \"--workspaces=false\"],\n                 capture_output=True, text=True, cwd=str(PROJECT_ROOT)\n             )\n             if result.returncode == 0:\n                 _print_success(\"    Node.js dependencies installed\")\n             else:\n                 from hermes_constants import display_hermes_home\n-                _print_warning(f\"    npm install failed - run manually: cd {display_hermes_home()}/hermes-agent && npm install\")\n+                _print_warning(f\"    npm install failed - run manually: cd {display_hermes_home()}/hermes-agent && npm install --workspaces=false\")\n                 if result.stderr:\n                     _print_info(f\"      {result.stderr.strip()[:200]}\")\n         elif not node_modules.exists():","@@ -951,13 +954,14 @@ def _run_post_setup(post_setup_key: str):\n             import subprocess\n             # Absolute npm path so .cmd shim executes on Windows.\n             result = subprocess.run(\n-                [_npm_bin, \"install\", \"--silent\"],\n+                # --workspaces=false avoids resolving apps/desktop. See #38772.\n+                [_npm_bin, \"install\", \"--silent\", \"--workspaces=false\"],\n                 capture_output=True, text=True, cwd=str(PROJECT_ROOT)\n             )\n             if result.returncode == 0:\n                 _print_success(\"    Camofox installed\")\n             else:\n-                _print_warning(\"    npm install failed - run manually: npm install\")\n+                _print_warning(\"    npm install failed - run manually: npm install --workspaces=false\")\n         if camofox_dir.exists():\n             _print_info(\"    Start the Camofox server:\")\n             _print_info(\"      npx @askjo/camofox-browser\")"]},{"file":"package.json","hunks":["@@ -10,7 +10,17 @@\n     \"web\"\n   ],\n   \"scripts\": {\n-    \"postinstall\": \"echo '✅ Browser tools ready. Run: python run_agent.py --help'\"\n+    \"postinstall\": \"echo '✅ Browser tools ready. Run: python run_agent.py --help'\",\n+    \"install:root\": \"npm install --workspaces=false\",\n+    \"install:web\": \"npm install --workspace web\",\n+    \"install:tui\": \"npm install --workspace ui-tui\",\n+    \"install:desktop\": \"npm install --workspace apps/desktop\",\n+    \"audit:root\": \"npm audit --workspaces=false\",\n+    \"audit:web\": \"npm audit --workspace web\",\n+    \"audit:tui\": \"npm audit --workspace ui-tui\",\n+    \"audit:fix:root\": \"npm audit fix --workspaces=false\",\n+    \"audit:fix:web\": \"npm audit fix --workspace web\",\n+    \"audit:fix:tui\": \"npm audit fix --workspace ui-tui\"\n   },\n   \"repository\": {\n     \"type\": \"git\","]},{"file":"scripts/release.py","hunks":["@@ -46,6 +46,7 @@\n # Auto-extracted from noreply emails + manual overrides\n AUTHOR_MAP = {\n     \"[redacted:email]\": \"randomsnowflake\",\n+    \"[redacted:email]\": \"zakame\",\n     \"[redacted:email]\": \"ViewWay\",\n     \"[redacted:email]\": \"harjothkhara\",\n     \"129007007+HeLLGURD@users.noreply.github.com\": \"HeLLGURD\","]},{"file":"tests/hermes_cli/test_cmd_update.py","hunks":["@@ -272,7 +272,7 @@ def test_update_refreshes_repo_and_tui_node_dependencies(\n             # The web/ install runs from the workspace root when the root\n             # lockfile exists (npm workspaces hoist node_modules upward).\n             assert npm_calls[2:] == [\n-                ([\"/usr/bin/npm\", \"ci\", \"--silent\"], PROJECT_ROOT),\n+                ([\"/usr/bin/npm\", \"ci\", \"--workspace\", \"web\", \"--silent\"], PROJECT_ROOT),\n             ]\n \n         # The web UI build itself went through the streaming helper."]},{"file":"tests/hermes_cli/test_tui_npm_install.py","hunks":["@@ -255,6 +255,8 @@ def fake_run(*args, **kwargs):\n     assert calls[0][0][0] == [\n         \"/bin/npm\",\n         \"install\",\n+        \"--workspace\",\n+        \"ui-tui\",\n         \"--silent\",\n         \"--no-fund\",\n         \"--no-audit\",","@@ -390,3 +392,36 @@ def test_no_stray_lockfiles_in_workspace_subdirs(main_mod) -> None:\n         \"delete them and run `npm install` from the repo root instead: \"\n         + \", \".join(str(d / \"package-lock.json\") for d in stray)\n     )\n+\n+\n+def test_tui_launch_install_uses_workspace_scope(\n+    tmp_path: Path, main_mod, monkeypatch\n+) -> None:\n+    \"\"\"TUI launch npm install must pass --workspace ui-tui to avoid pulling apps/desktop.\"\"\"\n+    tui_dir = tmp_path / \"ui-tui\"\n+    tui_dir.mkdir()\n+    (tui_dir / \"package.json\").write_text(\"{}\")\n+    (tui_dir / \"dist\" / \"entry.js\").parent.mkdir(parents=True)\n+    (tui_dir / \"dist\" / \"entry.js\").write_text(\"console.log('tui')\")\n+    # workspace root: parent has lockfile, tui_dir does not\n+    (tmp_path / \"package-lock.json\").write_text(\"{}\")\n+\n+    monkeypatch.setattr(main_mod, \"_tui_need_npm_install\", lambda _root: True)\n+    monkeypatch.setattr(main_mod, \"_tui_need_rebuild\", lambda _root: False)\n+    monkeypatch.setattr(main_mod.shutil, \"which\", lambda name: f\"/usr/bin/{name}\")\n+\n+    npm_calls = []\n+\n+    def fake_run(cmd, **kwargs):\n+        if cmd[0].endswith(\"npm\"):\n+            npm_calls.append(cmd)\n+        return types.SimpleNamespace(returncode=0, stdout=\"\", stderr=\"\")\n+\n+    monkeypatch.setattr(main_mod.subprocess, \"run\", fake_run)\n+\n+    main_mod._make_tui_argv(tui_dir, tui_dev=False)\n+\n+    assert npm_calls, \"expected npm install to be called\"\n+    install_cmd = npm_calls[0]\n+    assert \"--workspace\" in install_cmd\n+    assert \"ui-tui\" in install_cmd"]},{"file":"tests/hermes_cli/test_web_ui_build.py","hunks":["@@ -140,6 +140,19 @@ def test_npm_install_uses_utf8_replace_output_decoding(self, tmp_path):\n         assert kwargs[\"encoding\"] == \"utf-8\"\n         assert kwargs[\"errors\"] == \"replace\"\n \n+    def test_npm_install_uses_workspace_web_scope(self, tmp_path):\n+        web_dir, _ = _make_web_dir(tmp_path)\n+        mock_cp = __import__(\"subprocess\").CompletedProcess([], 0, stdout=\"\", stderr=\"\")\n+        build_ok = __import__(\"subprocess\").CompletedProcess([], 0, stdout=\"\", stderr=\"\")\n+        with patch(\"hermes_cli.main.shutil.which\", return_value=\"/usr/bin/npm\"), \\\n+             patch(\"hermes_cli.main.subprocess.run\", return_value=mock_cp) as mock_run, \\\n+             patch(\"hermes_cli.main._run_with_idle_timeout\", return_value=build_ok):\n+            result = _build_web_ui(web_dir)\n+        assert result is True\n+        install_cmd = mock_run.call_args[0][0]\n+        assert \"--workspace\" in install_cmd\n+        assert install_cmd[install_cmd.index(\"--workspace\") + 1] == \"web\"\n+\n     def test_web_build_uses_idle_timeout_helper(self, tmp_path):\n         \"\"\"npm run build now goes through _run_with_idle_timeout (issue #33788).\n ","@@ -205,7 +218,7 @@ def test_desktop_web_install_uses_existing_workspace_root(\n \n         assert result is True\n         args, kwargs = mock_run.call_args\n-        assert args[0] == [\"/usr/bin/npm\", \"ci\", \"--silent\"]\n+        assert args[0] == [\"/usr/bin/npm\", \"ci\", \"--workspace\", \"web\", \"--silent\"]\n         assert kwargs[\"cwd\"] == tmp_path\n \n \n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}