{"repo":"NousResearch/hermes-agent","public_id":"42065","display_label":"Gateway install headless/non-TTY prompt handling","summary":"These issues and PRs address a regression where `hermes gateway install` requires interactive prompts even in non-TTY or headless environments, and add a `--yes` flag plus fixes to honor non-interactive context and startup flags during systemd installs.","subsystem":"hermes_cli","counts":{"members":5,"open_issues":1,"open_issues_distinct":1,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":54163,"merged_at":"2026-06-28T11:09:55+00:00"},"resolution":{"state":"partially_resolved","by":"pr-54163"},"freshness":{"state":"verified","last_verified_at":"2026-06-28T11:09:55+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.33599999999999997},"fix_edges":[{"issue_number":42065,"pr_number":54163,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-28T11:09:55+00:00"}],"members":[{"kind":"issue","number":42065,"title":"`hermes gateway install` is non-bypassable interactive on systemd — breaks headless installs (regression in v2026.5.28)","url":null,"state":"closed"},{"kind":"issue","number":60244,"title":"Feature: hermes gateway install --yes for headless provisioning (+ piped-stdin prompt quirk on Windows)","url":null,"state":"open"},{"kind":"pr","number":42071,"title":"fix(gateway): honor --start-now/--start-on-login flags and support non-TTY headless installs","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"831d443b0","number":0},{"kind":"pr","value":"#54163","number":54163},{"kind":"pr","value":"#42124","number":42124}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":42124,"title":"fix(gateway): skip interactive prompts in non-TTY for gateway install (#42065)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#54163","number":54163},{"kind":"pr","value":"#42071","number":42071}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":54163,"title":"fix(gateway): headless systemd install — honor --start flags + non-TTY defaults (#42065)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/gateway.py","hermes_cli/subcommands/gateway.py","tests/hermes_cli/test_gateway.py"],"fix_hunks":[{"file":"hermes_cli/gateway.py","hunks":["@@ -3044,6 +3044,7 @@ def systemd_install(\n     system: bool = False,\n     run_as_user: str | None = None,\n     enable_on_startup: bool = True,\n+    non_interactive: bool = False,\n ):\n     if system:\n         _require_root_for_system_service(\"install\")","@@ -3057,7 +3058,7 @@ def systemd_install(\n         print()\n         print_legacy_unit_warning()\n         print()\n-        if prompt_yes_no(\"Remove the legacy unit(s) before installing?\", True):\n+        if non_interactive or prompt_yes_no(\"Remove the legacy unit(s) before installing?\", True):\n             remove_legacy_hermes_units(interactive=False)\n             print()\n ","@@ -6268,13 +6269,31 @@ def _gateway_command_inner(args):\n                     \"  Or use tmux/screen for persistence: tmux new -s hermes 'hermes gateway run'\"\n                 )\n                 print()\n-            start_now = prompt_yes_no(\"Start the gateway now after installing the service?\", True)\n-            start_on_login = prompt_yes_no(\"Start the gateway automatically on login/boot with systemd?\", True)\n+            # Honor CLI flags (--start-now / --no-start-now, --start-on-login /\n+            # --no-start-on-login).  When not provided, prompt interactively or\n+            # fall back to True for non-TTY / headless contexts (SSH, CI, pipes).\n+            non_interactive = not (hasattr(sys.stdin, \"isatty\") and sys.stdin.isatty())\n+            _sn = getattr(args, \"start_now\", None)\n+            if _sn is not None:\n+                start_now = _sn\n+            elif not non_interactive:\n+                start_now = prompt_yes_no(\"Start the gateway now after installing the service?\", True)\n+            else:\n+                start_now = True\n+\n+            _sol = getattr(args, \"start_on_login\", None)\n+            if _sol is not None:\n+                start_on_login = _sol\n+            elif not non_interactive:\n+                start_on_login = prompt_yes_no(\"Start the gateway automatically on login/boot with systemd?\", True)\n+            else:\n+                start_on_login = True\n             systemd_install(\n                 force=force,\n                 system=system,\n                 run_as_user=run_as_user,\n                 enable_on_startup=start_on_login,\n+                non_interactive=non_interactive,\n             )\n             if start_now:\n                 systemd_start(system=system)"]},{"file":"hermes_cli/subcommands/gateway.py","hunks":["@@ -169,26 +169,26 @@ def build_gateway_parser(\n         dest=\"start_now\",\n         action=\"store_true\",\n         default=None,\n-        help=argparse.SUPPRESS,\n+        help=\"Start the gateway service immediately after installing\",\n     )\n     gateway_install.add_argument(\n         \"--no-start-now\",\n         dest=\"start_now\",\n         action=\"store_false\",\n-        help=argparse.SUPPRESS,\n+        help=\"Do not start the gateway service after installing\",\n     )\n     gateway_install.add_argument(\n         \"--start-on-login\",\n         dest=\"start_on_login\",\n         action=\"store_true\",\n         default=None,\n-        help=argparse.SUPPRESS,\n+        help=\"Enable the service to start automatically on login/boot\",\n     )\n     gateway_install.add_argument(\n         \"--no-start-on-login\",\n         dest=\"start_on_login\",\n         action=\"store_false\",\n-        help=argparse.SUPPRESS,\n+        help=\"Do not enable the service to start on login/boot\",\n     )\n     gateway_install.add_argument(\n         \"--elevated-handoff\","]},{"file":"tests/hermes_cli/test_gateway.py","hunks":["@@ -444,13 +444,14 @@ def test_gateway_install_in_container_with_operational_systemd_uses_systemd(monk\n     monkeypatch.setattr(gateway, \"is_wsl\", lambda: False)\n     monkeypatch.setattr(gateway, \"is_macos\", lambda: False)\n     monkeypatch.setattr(gateway, \"is_managed\", lambda: False)\n+    monkeypatch.setattr(\"sys.stdin.isatty\", lambda: True)\n \n     calls = []\n     monkeypatch.setattr(gateway, \"prompt_yes_no\", lambda question, default=True: calls.append((\"prompt\", question, default)) or True)\n     monkeypatch.setattr(\n         gateway,\n         \"systemd_install\",\n-        lambda force=False, system=False, run_as_user=None, enable_on_startup=True: calls.append((\"install\", force, system, run_as_user, enable_on_startup)),\n+        lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append((\"install\", force, system, run_as_user, enable_on_startup)),\n     )\n     monkeypatch.setattr(gateway, \"systemd_start\", lambda system=False: calls.append((\"start\", system)))\n ","@@ -766,7 +767,7 @@ def test_install_linux_gateway_from_setup_system_choice_as_root_installs(monkeyp\n     monkeypatch.setattr(\n         gateway,\n         \"systemd_install\",\n-        lambda force=False, system=False, run_as_user=None, enable_on_startup=True: calls.append((force, system, run_as_user, enable_on_startup)),\n+        lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append((force, system, run_as_user, enable_on_startup)),\n     )\n \n     scope, did_install = gateway.install_linux_gateway_from_setup(force=True)","@@ -782,7 +783,7 @@ def test_install_linux_gateway_from_setup_passes_startup_choice(monkeypatch):\n     monkeypatch.setattr(\n         gateway,\n         \"systemd_install\",\n-        lambda force=False, system=False, run_as_user=None, enable_on_startup=True: calls.append((force, system, run_as_user, enable_on_startup)),\n+        lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append((force, system, run_as_user, enable_on_startup)),\n     )\n \n     scope, did_install = gateway.install_linux_gateway_from_setup(force=False, enable_on_startup=False)","@@ -796,6 +797,7 @@ def test_gateway_install_can_decline_start_now_and_startup(monkeypatch):\n     monkeypatch.setattr(gateway, \"is_wsl\", lambda: False)\n     monkeypatch.setattr(gateway, \"is_macos\", lambda: False)\n     monkeypatch.setattr(gateway, \"is_managed\", lambda: False)\n+    monkeypatch.setattr(\"sys.stdin.isatty\", lambda: True)\n \n     answers = iter([False, False])\n     calls = []","@@ -803,7 +805,7 @@ def test_gateway_install_can_decline_start_now_and_startup(monkeypatch):\n     monkeypatch.setattr(\n         gateway,\n         \"systemd_install\",\n-        lambda force=False, system=False, run_as_user=None, enable_on_startup=True: calls.append((\"install\", force, system, run_as_user, enable_on_startup)),\n+        lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append((\"install\", force, system, run_as_user, enable_on_startup)),\n     )\n     monkeypatch.setattr(gateway, \"systemd_start\", lambda system=False: calls.append((\"start\", system)))\n ","@@ -817,6 +819,119 @@ def test_gateway_install_can_decline_start_now_and_startup(monkeypatch):\n     ]\n \n \n+def test_gateway_install_systemd_honors_start_now_flag(monkeypatch):\n+    \"\"\"--start-now / --no-start-now should bypass the interactive prompt.\"\"\"\n+    monkeypatch.setattr(gateway, \"supports_systemd_services\", lambda: True)\n+    monkeypatch.setattr(gateway, \"is_wsl\", lambda: False)\n+    monkeypatch.setattr(gateway, \"is_macos\", lambda: False)\n+    monkeypatch.setattr(gateway, \"is_managed\", lambda: False)\n+\n+    calls = []\n+    monkeypatch.setattr(gateway, \"prompt_yes_no\", lambda question, default=True: calls.append((\"prompt\", question)))\n+    monkeypatch.setattr(\n+        gateway,\n+        \"systemd_install\",\n+        lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append((\"install\", enable_on_startup)),\n+    )\n+    monkeypatch.setattr(gateway, \"systemd_start\", lambda system=False: calls.append((\"start\",)))\n+\n+    args = SimpleNamespace(\n+        gateway_command=\"install\", force=False, system=False,\n+        run_as_user=None, start_now=True, start_on_login=False,\n+    )\n+    gateway.gateway_command(args)\n+\n+    assert (\"prompt\", \"Start the gateway now after installing the service?\") not in calls\n+    assert (\"start\",) in calls\n+    assert (\"install\", False) in calls\n+\n+\n+def test_gateway_install_systemd_non_tty_uses_defaults(monkeypatch):\n+    \"\"\"Non-TTY stdin (headless/CI) should use True defaults without prompting.\"\"\"\n+    monkeypatch.setattr(gateway, \"supports_systemd_services\", lambda: True)\n+    monkeypatch.setattr(gateway, \"is_wsl\", lambda: False)\n+    monkeypatch.setattr(gateway, \"is_macos\", lambda: False)\n+    monkeypatch.setattr(gateway, \"is_managed\", lambda: False)\n+    monkeypatch.setattr(\"sys.stdin.isatty\", lambda: False)\n+\n+    calls = []\n+    monkeypatch.setattr(gateway, \"prompt_yes_no\", lambda question, default=True: calls.append((\"prompt\", question)))\n+    monkeypatch.setattr(\n+        gateway,\n+        \"systemd_install\",\n+        lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append((\"install\", enable_on_startup)),\n+    )\n+    monkeypatch.setattr(gateway, \"systemd_start\", lambda system=False: calls.append((\"start\",)))\n+\n+    args = SimpleNamespace(gateway_command=\"install\", force=False, system=False, run_as_user=None)\n+    gateway.gateway_command(args)\n+\n+    # No prompts — defaults used (start_now=True, start_on_login=True)\n+    assert all(c[0] != \"prompt\" for c in calls)\n+    assert (\"install\", True) in calls\n+    assert (\"start\",) in calls\n+\n+\n+def test_gateway_install_systemd_no_start_now_flag_non_tty(monkeypatch):\n+    \"\"\"--no-start-now in non-TTY should skip starting the service.\"\"\"\n+    monkeypatch.setattr(gateway, \"supports_systemd_services\", lambda: True)\n+    monkeypatch.setattr(gateway, \"is_wsl\", lambda: False)\n+    monkeypatch.setattr(gateway, \"is_macos\", lambda: False)\n+    monkeypatch.setattr(gateway, \"is_managed\", lambda: False)\n+    monkeypatch.setattr(\"sys.stdin.isatty\", lambda: False)\n+\n+    calls = []\n+    monkeypatch.setattr(gateway, \"prompt_yes_no\", lambda question, default=True: calls.append((\"prompt\", question)))\n+    monkeypatch.setattr(\n+        gateway,\n+        \"systemd_install\",\n+        lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append((\"install\", enable_on_startup)),\n+    )\n+    monkeypatch.setattr(gateway, \"systemd_start\", lambda system=False: calls.append((\"start\",)))\n+\n+    args = SimpleNamespace(\n+        gateway_command=\"install\", force=False, system=False,\n+        run_as_user=None, start_now=False, start_on_login=True,\n+    )\n+    gateway.gateway_command(args)\n+\n+    assert all(c[0] != \"prompt\" for c in calls)\n+    assert (\"install\", True) in calls\n+    assert (\"start\",) not in calls\n+\n+\n+def test_gateway_install_noninteractive_skips_legacy_unit_prompt(monkeypatch, tmp_path):\n+    \"\"\"In non-TTY, the legacy-unit removal prompt in systemd_install is skipped.\n+\n+    Covers the second hidden prompt that --start-now/--start-on-login do not\n+    guard. Originally contributed via PR #42124 (kyssta-exe).\n+    \"\"\"\n+    monkeypatch.setattr(gateway, \"has_legacy_hermes_units\", lambda: True)\n+\n+    calls = []\n+    monkeypatch.setattr(\n+        gateway,\n+        \"prompt_yes_no\",\n+        lambda question, default=True: calls.append((\"prompt\", question)) or True,\n+    )\n+    monkeypatch.setattr(gateway, \"remove_legacy_hermes_units\", lambda interactive=False: calls.append((\"remove_legacy\",)))\n+    monkeypatch.setattr(gateway, \"print_legacy_unit_warning\", lambda: None)\n+\n+    fake_path = tmp_path / \"hermes-gateway.service\"\n+    monkeypatch.setattr(gateway, \"get_systemd_unit_path\", lambda system=False: fake_path)\n+    monkeypatch.setattr(gateway, \"generate_systemd_unit\", lambda system=False, run_as_user=None: \"[Service]\")\n+    monkeypatch.setattr(gateway, \"_run_systemctl\", lambda *a, **kw: None)\n+    monkeypatch.setattr(gateway, \"_ensure_linger_enabled\", lambda: None)\n+    monkeypatch.setattr(gateway, \"print_systemd_scope_conflict_warning\", lambda: None)\n+    monkeypatch.setattr(gateway, \"_service_scope_label\", lambda system=False: \"user\")\n+\n+    gateway.systemd_install(non_interactive=True)\n+\n+    # Legacy units removed without prompting.\n+    assert (\"remove_legacy\",) in calls\n+    assert all(c[0] != \"prompt\" for c in calls)\n+\n+\n def test_find_gateway_pids_falls_back_to_pid_file_when_process_scan_fails(monkeypatch):\n     monkeypatch.setattr(gateway, \"_get_service_pids\", lambda: set())\n     monkeypatch.setattr(gateway, \"is_windows\", lambda: False)\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}