# NousResearch/hermes-agent · Gateway install headless/non-TTY prompt handling

## Open now

- 1 issue, 0 PRs open on GitHub.
- #60244: Feature: hermes gateway install --yes for headless provisioning (+ piped-stdin prompt quirk on Windows)

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.

## TL;DR

- 5 member(s): 1 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 #54163. Verify it addresses your case.
- **Recommended:** read PR #54163 (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: `partially_resolved` · deciding PR(s): `pr-54163`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.34`: 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.

## Open related issues

- #60244: Feature: hermes gateway install --yes for headless provisioning (+ piped-stdin prompt quirk on Windows)

## Closed / duplicate issues

- #42065: \`hermes gateway install\` is non-bypassable interactive on systemd — breaks headless installs (regression in v2026.5.28)

## Prior & competing PRs

- **#54163: fix(gateway): headless systemd install — honor --start flags + non-TTY defaults (#42065)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `hermes_cli/gateway.py`, `hermes_cli/subcommands/gateway.py`, `tests/hermes_cli/test_gateway.py`
  - fix hunks:
  - `hermes_cli/gateway.py`

    ```diff
    @@ -3044,6 +3044,7 @@ def systemd_install(
         system: bool = False,
         run_as_user: str | None = None,
         enable_on_startup: bool = True,
    +    non_interactive: bool = False,
     ):
         if system:
             _require_root_for_system_service("install")
    ```

    ```diff
    @@ -3057,7 +3058,7 @@ def systemd_install(
             print()
             print_legacy_unit_warning()
             print()
    -        if prompt_yes_no("Remove the legacy unit(s) before installing?", True):
    +        if non_interactive or prompt_yes_no("Remove the legacy unit(s) before installing?", True):
                 remove_legacy_hermes_units(interactive=False)
                 print()
     
    ```

    ```diff
    @@ -6268,13 +6269,31 @@ def _gateway_command_inner(args):
                         "  Or use tmux/screen for persistence: tmux new -s hermes 'hermes gateway run'"
                     )
                     print()
    -            start_now = prompt_yes_no("Start the gateway now after installing the service?", True)
    -            start_on_login = prompt_yes_no("Start the gateway automatically on login/boot with systemd?", True)
    +            # Honor CLI flags (--start-now / --no-start-now, --start-on-login /
    +            # --no-start-on-login).  When not provided, prompt interactively or
    +            # fall back to True for non-TTY / headless contexts (SSH, CI, pipes).
    +            non_interactive = not (hasattr(sys.stdin, "isatty") and sys.stdin.isatty())
    +            _sn = getattr(args, "start_now", None)
    +            if _sn is not None:
    +                start_now = _sn
    +            elif not non_interactive:
    +                start_now = prompt_yes_no("Start the gateway now after installing the service?", True)
    +            else:
    +                start_now = True
    +
    +            _sol = getattr(args, "start_on_login", None)
    +            if _sol is not None:
    +                start_on_login = _sol
    +            elif not non_interactive:
    +                start_on_login = prompt_yes_no("Start the gateway automatically on login/boot with systemd?", True)
    +            else:
    +                start_on_login = True
                 systemd_install(
                     force=force,
                     system=system,
                     run_as_user=run_as_user,
                     enable_on_startup=start_on_login,
    +                non_interactive=non_interactive,
                 )
                 if start_now:
                     systemd_start(system=system)
    ```
  - `hermes_cli/subcommands/gateway.py`

    ```diff
    @@ -169,26 +169,26 @@ def build_gateway_parser(
             dest="start_now",
             action="store_true",
             default=None,
    -        help=argparse.SUPPRESS,
    +        help="Start the gateway service immediately after installing",
         )
         gateway_install.add_argument(
             "--no-start-now",
             dest="start_now",
             action="store_false",
    -        help=argparse.SUPPRESS,
    +        help="Do not start the gateway service after installing",
         )
         gateway_install.add_argument(
             "--start-on-login",
             dest="start_on_login",
             action="store_true",
             default=None,
    -        help=argparse.SUPPRESS,
    +        help="Enable the service to start automatically on login/boot",
         )
         gateway_install.add_argument(
             "--no-start-on-login",
             dest="start_on_login",
             action="store_false",
    -        help=argparse.SUPPRESS,
    +        help="Do not enable the service to start on login/boot",
         )
         gateway_install.add_argument(
             "--elevated-handoff",
    ```
  - `tests/hermes_cli/test_gateway.py`

    ```diff
    @@ -444,13 +444,14 @@ def test_gateway_install_in_container_with_operational_systemd_uses_systemd(monk
         monkeypatch.setattr(gateway, "is_wsl", lambda: False)
         monkeypatch.setattr(gateway, "is_macos", lambda: False)
         monkeypatch.setattr(gateway, "is_managed", lambda: False)
    +    monkeypatch.setattr("sys.stdin.isatty", lambda: True)
     
         calls = []
         monkeypatch.setattr(gateway, "prompt_yes_no", lambda question, default=True: calls.append(("prompt", question, default)) or True)
         monkeypatch.setattr(
             gateway,
             "systemd_install",
    -        lambda force=False, system=False, run_as_user=None, enable_on_startup=True: calls.append(("install", force, system, run_as_user, enable_on_startup)),
    +        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)),
         )
         monkeypatch.setattr(gateway, "systemd_start", lambda system=False: calls.append(("start", system)))
     
    ```

    ```diff
    @@ -766,7 +767,7 @@ def test_install_linux_gateway_from_setup_system_choice_as_root_installs(monkeyp
         monkeypatch.setattr(
             gateway,
             "systemd_install",
    -        lambda force=False, system=False, run_as_user=None, enable_on_startup=True: calls.append((force, system, run_as_user, enable_on_startup)),
    +        lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append((force, system, run_as_user, enable_on_startup)),
         )
     
         scope, did_install = gateway.install_linux_gateway_from_setup(force=True)
    ```

    ```diff
    @@ -782,7 +783,7 @@ def test_install_linux_gateway_from_setup_passes_startup_choice(monkeypatch):
         monkeypatch.setattr(
             gateway,
             "systemd_install",
    -        lambda force=False, system=False, run_as_user=None, enable_on_startup=True: calls.append((force, system, run_as_user, enable_on_startup)),
    +        lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append((force, system, run_as_user, enable_on_startup)),
         )
     
         scope, did_install = gateway.install_linux_gateway_from_setup(force=False, enable_on_startup=False)
    ```

    ```diff
    @@ -796,6 +797,7 @@ def test_gateway_install_can_decline_start_now_and_startup(monkeypatch):
         monkeypatch.setattr(gateway, "is_wsl", lambda: False)
         monkeypatch.setattr(gateway, "is_macos", lambda: False)
         monkeypatch.setattr(gateway, "is_managed", lambda: False)
    +    monkeypatch.setattr("sys.stdin.isatty", lambda: True)
     
         answers = iter([False, False])
         calls = []
    ```

    ```diff
    @@ -803,7 +805,7 @@ def test_gateway_install_can_decline_start_now_and_startup(monkeypatch):
         monkeypatch.setattr(
             gateway,
             "systemd_install",
    -        lambda force=False, system=False, run_as_user=None, enable_on_startup=True: calls.append(("install", force, system, run_as_user, enable_on_startup)),
    +        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)),
         )
         monkeypatch.setattr(gateway, "systemd_start", lambda system=False: calls.append(("start", system)))
     
    ```

    ```diff
    @@ -817,6 +819,119 @@ def test_gateway_install_can_decline_start_now_and_startup(monkeypatch):
         ]
     
     
    +def test_gateway_install_systemd_honors_start_now_flag(monkeypatch):
    +    """--start-now / --no-start-now should bypass the interactive prompt."""
    +    monkeypatch.setattr(gateway, "supports_systemd_services", lambda: True)
    +    monkeypatch.setattr(gateway, "is_wsl", lambda: False)
    +    monkeypatch.setattr(gateway, "is_macos", lambda: False)
    +    monkeypatch.setattr(gateway, "is_managed", lambda: False)
    +
    +    calls = []
    +    monkeypatch.setattr(gateway, "prompt_yes_no", lambda question, default=True: calls.append(("prompt", question)))
    +    monkeypatch.setattr(
    +        gateway,
    +        "systemd_install",
    +        lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append(("install", enable_on_startup)),
    +    )
    +    monkeypatch.setattr(gateway, "systemd_start", lambda system=False: calls.append(("start",)))
    +
    +    args = SimpleNamespace(
    +        gateway_command="install", force=False, system=False,
    +        run_as_user=None, start_now=True, start_on_login=False,
    +    )
    +    gateway.gateway_command(args)
    +
    +    assert ("prompt", "Start the gateway now after installing the service?") not in calls
    +    assert ("start",) in calls
    +    assert ("install", False) in calls
    +
    +
    +def test_gateway_install_systemd_non_tty_uses_defaults(monkeypatch):
    +    """Non-TTY stdin (headless/CI) should use True defaults without prompting."""
    +    monkeypatch.setattr(gateway, "supports_systemd_services", lambda: True)
    +    monkeypatch.setattr(gateway, "is_wsl", lambda: False)
    +    monkeypatch.setattr(gateway, "is_macos", lambda: False)
    +    monkeypatch.setattr(gateway, "is_managed", lambda: False)
    +    monkeypatch.setattr("sys.stdin.isatty", lambda: False)
    +
    +    calls = []
    +    monkeypatch.setattr(gateway, "prompt_yes_no", lambda question, default=True: calls.append(("prompt", question)))
    +    monkeypatch.setattr(
    +        gateway,
    +        "systemd_install",
    +        lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append(("install", enable_on_startup)),
    +    )
    +    monkeypatch.setattr(gateway, "systemd_start", lambda system=False: calls.append(("start",)))
    +
    +    args = SimpleNamespace(gateway_command="install", force=False, system=False, run_as_user=None)
    +    gateway.gateway_command(args)
    +
    +    # No prompts — defaults used (start_now=True, start_on_login=True)
    +    assert all(c[0] != "prompt" for c in calls)
    +    assert ("install", True) in calls
    +    assert ("start",) in calls
    +
    +
    +def test_gateway_install_systemd_no_start_now_flag_non_tty(monkeypatch):
    +    """--no-start-now in non-TTY should skip starting the service."""
    +    monkeypatch.setattr(gateway, "supports_systemd_services", lambda: True)
    +    monkeypatch.setattr(gateway, "is_wsl", lambda: False)
    +    monkeypatch.setattr(gateway, "is_macos", lambda: False)
    +    monkeypatch.setattr(gateway, "is_managed", lambda: False)
    +    monkeypatch.setattr("sys.stdin.isatty", lambda: False)
    +
    +    calls = []
    +    monkeypatch.setattr(gateway, "prompt_yes_no", lambda question, default=True: calls.append(("prompt", question)))
    +    monkeypatch.setattr(
    +        gateway,
    +        "systemd_install",
    +        lambda force=False, system=False, run_as_user=None, enable_on_startup=True, **kw: calls.append(("install", enable_on_startup)),
    +    )
    +    monkeypatch.setattr(gateway, "systemd_start", lambda system=False: calls.append(("start",)))
    +
    +    args = SimpleNamespace(
    +        gateway_command="install", force=False, system=False,
    +        run_as_user=None, start_now=False, start_on_login=True,
    +    )
    +    gateway.gateway_command(args)
    +
    +    assert all(c[0] != "prompt" for c in calls)
    +    assert ("install", True) in calls
    +    assert ("start",) not in calls
    +
    +
    +def test_gateway_install_noninteractive_skips_legacy_unit_prompt(monkeypatch, tmp_path):
    +    """In non-TTY, the legacy-unit removal prompt in systemd_install is skipped.
    +
    +    Covers the second hidden prompt that --start-now/--start-on-login do not
    +    guard. Originally contributed via PR #42124 (kyssta-exe).
    +    """
    +    monkeypatch.setattr(gateway, "has_legacy_hermes_units", lambda: True)
    +
    +    calls = []
    +    monkeypatch.setattr(
    +        gateway,
    +        "prompt_yes_no",
    +        lambda question, default=True: calls.append(("prompt", question)) or True,
    +    )
    +    monkeypatch.setattr(gateway, "remove_legacy_hermes_units", lambda interactive=False: calls.append(("remove_legacy",)))
    +    monkeypatch.setattr(gateway, "print_legacy_unit_warning", lambda: None)
    +
    +    fake_path = tmp_path / "hermes-gateway.service"
    +    monkeypatch.setattr(gateway, "get_systemd_unit_path", lambda system=False: fake_path)
    +    monkeypatch.setattr(gateway, "generate_systemd_unit", lambda system=False, run_as_user=None: "[Service]")
    +    monkeypatch.setattr(gateway, "_run_systemctl", lambda *a, **kw: None)
    +    monkeypatch.setattr(gateway, "_ensure_linger_enabled", lambda: None)
    +    monkeypatch.setattr(gateway, "print_systemd_scope_conflict_warning", lambda: None)
    +    monkeypatch.setattr(gateway, "_service_scope_label", lambda system=False: "user")
    +
    +    gateway.systemd_install(non_interactive=True)
    +
    +    # Legacy units removed without prompting.
    +    assert ("remove_legacy",) in calls
    +    assert all(c[0] != "prompt" for c in calls)
    +
    +
     def test_find_gateway_pids_falls_back_to_pid_file_when_process_scan_fails(monkeypatch):
         monkeypatch.setattr(gateway, "_get_service_pids", lambda: set())
         monkeypatch.setattr(gateway, "is_windows", lambda: False)
    ```
- **#42071: fix(gateway): honor --start-now/--start-on-login flags and support non-TTY headless installs**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `831d443b0`, `#54163`, `#42124`
- **#42124: fix(gateway): skip interactive prompts in non-TTY for gateway install (#42065)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#54163`, `#42071`

## Fix status

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