NousResearch/hermes-agent

Gateway install headless/non-TTY prompt handling

Cluster 42065 · hermes cli. Data as of

1 open item. Fix merged in #54163 on 28 Jun 2026. Check it covers your case.

The short answer

1open issue
0open PRs
5items grouped
Known?
Yes. 5 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #54163. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #54163. That is separate from the fix event.
Still open
1 issue and 0 PRs.

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.

Open issues1

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

Open PRs0

No open pull requests.

Fixes on record

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

  • #54163 fix(gateway): headless systemd install — honor --start flags + non-TTY defaults (#42065)

    merged to main

    files: hermes_cli/gateway.py, hermes_cli/subcommands/gateway.py, tests/hermes_cli/test_gateway.py

    Show the diff
    • hermes_cli/gateway.py
      @@ -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")
      @@ -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()
       
      @@ -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
      @@ -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
      @@ -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)))
       
      @@ -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)
      @@ -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)
      @@ -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 = []
      @@ -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)))
       
      @@ -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)

2 PRs closed without merging

From the close comments: merged via another change 2

  • #42071 fix(gateway): honor --start-now/--start-on-login flags and support non-TTY headless installs · merged via another change → 831d443, #54163, #42124
  • #42124 fix(gateway): skip interactive prompts in non-TTY for gateway install (#42065) · merged via another change → #54163, #42071
Closed issues · 1
  • Closed#42065`hermes gateway install` is non-bypassable interactive on systemd — breaks headless installs (regression in v2026.5.28)
How this cluster was grouped
  • Link evidence (partly structural links): 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 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

Subsystem
hermes cli
Merged PRs
#54163
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
28 Jun 2026

For your agent

The same facts, typed.