Nothing open on GitHub. Fix merged in #40543 on 7 Jun 2026. Check it covers your case.
The short answer
- 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
resolved, deciding PR #40543. That is separate from the fix event. - Still open
- Nothing open on GitHub.
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.
Open issues0
Open PRs0
Fixes on record
-
#40543 fix(install): scope npm installs/audits to avoid pulling in apps/desktop (#38358)
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.pyShow 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: superseded by another PR 1
- #38396 fix(cli): scope web UI npm install to --workspace web · superseded by another PR
Closed issues · 1
- Closed#38358_build_web_ui() npm install missing --workspace web flag breaks "hermes update" on multi-workspace repos
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.00means 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 LabAt a glance
- Merged PRs
- #40543
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 7 Jun 2026