hermes - ✅(Solved) Fix [Bug]: launchd plist refresh reports success even when launchctl reload fails [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#12866Fetched 2026-04-20 12:16:33
View on GitHub
Comments
1
Participants
2
Timeline
3
Reactions
0
Participants
Timeline (top)
commented ×1cross-referenced ×1referenced ×1

Refreshing an outdated launchd plist always reports success even if launchctl bootout or launchctl bootstrap fails. That makes hermes gateway install/repair silently claim the service definition was updated while launchd may still be unloaded or broken.

Error Message

  • A failed bootout/bootstrap should surface as an error or a false return value.

Root Cause

Refreshing an outdated launchd plist always reports success even if launchctl bootout or launchctl bootstrap fails. That makes hermes gateway install/repair silently claim the service definition was updated while launchd may still be unloaded or broken.

Fix Action

Fix / Workaround

Minimal reproduction

from pathlib import Path
from tempfile import TemporaryDirectory
from types import SimpleNamespace
from unittest.mock import patch
from hermes_cli import gateway as g

with patch.object(g, "get_launchd_plist_path", return_value=plist), \
         patch.object(g, "launchd_plist_is_current", return_value=False), \
         patch.object(g, "generate_launchd_plist", return_value="new"), \
         patch.object(g, "get_launchd_label", return_value="com.hermes.agent"), \
         patch.object(g, "_launchd_domain", return_value="gui/501"), \
         patch("subprocess.run", side_effect=fake_run), \
         patch("builtins.print", side_effect=lambda *a, **k: printed.append(" ".join(str(x) for x in a))):
        result = g.refresh_launchd_plist_if_needed()

PR fix notes

PR #12882: fix(gateway): surface launchctl bootstrap failures in refresh_launchd_plist_if_needed

Description (problem / solution / changelog)

Problem

refresh_launchd_plist_if_needed() ran both launchctl commands with check=False, ignored their return codes, and always printed a success message and returned True — even when launchctl bootstrap failed.

This means hermes gateway install / repair silently claims the service definition was updated while launchd may still be unloaded or broken.

Fix

  • bootout: keep check=False — non-zero is harmless when the job was never loaded
  • bootstrap: capture returncode; return False and print a diagnostic message when non-zero, so install/repair does not claim success when launchd rejected the plist

Tests

Adds tests/hermes_cli/test_launchd_plist_refresh.py with 6 regression tests — all pass:

  • bootstrap success → returns True
  • bootstrap failure (rc=5) → returns False, no success message printed
  • bootout failure → tolerated, still returns True
  • plist already current → returns False
  • plist missing → returns False
  • bootstrap called with check=False

Fixes #12866

Changed files

  • hermes_cli/gateway.py (modified, +26/-3)
  • tests/hermes_cli/test_launchd_plist_refresh.py (added, +116/-0)

Code Example

from pathlib import Path
from tempfile import TemporaryDirectory
from types import SimpleNamespace
from unittest.mock import patch
from hermes_cli import gateway as g

calls = []
printed = []
with TemporaryDirectory() as td:
    plist = Path(td) / "com.hermes.agent.plist"
    plist.write_text("old", encoding="utf-8")

    def fake_run(cmd, check=False, timeout=None):
        calls.append({"cmd": cmd, "check": check, "timeout": timeout})
        return SimpleNamespace(returncode=5)

    with patch.object(g, "get_launchd_plist_path", return_value=plist), \
         patch.object(g, "launchd_plist_is_current", return_value=False), \
         patch.object(g, "generate_launchd_plist", return_value="new"), \
         patch.object(g, "get_launchd_label", return_value="com.hermes.agent"), \
         patch.object(g, "_launchd_domain", return_value="gui/501"), \
         patch("subprocess.run", side_effect=fake_run), \
         patch("builtins.print", side_effect=lambda *a, **k: printed.append(" ".join(str(x) for x in a))):
        result = g.refresh_launchd_plist_if_needed()

print(result)   # actual: True
print(printed)  # actual: success message still emitted
RAW_BUFFERClick to expand / collapse

Summary

Refreshing an outdated launchd plist always reports success even if launchctl bootout or launchctl bootstrap fails. That makes hermes gateway install/repair silently claim the service definition was updated while launchd may still be unloaded or broken.

Affected code

  • hermes_cli/gateway.py:1334-1340
  • hermes_cli/gateway.py:1346-1351
  • coverage gap: tests/hermes_cli/test_gateway_service.py:194-216

Why this is a bug

refresh_launchd_plist_if_needed() rewrites the plist and then runs both launchctl commands with check=False, ignoring non-zero return codes. The function always prints a success message and returns True.

Minimal reproduction

from pathlib import Path
from tempfile import TemporaryDirectory
from types import SimpleNamespace
from unittest.mock import patch
from hermes_cli import gateway as g

calls = []
printed = []
with TemporaryDirectory() as td:
    plist = Path(td) / "com.hermes.agent.plist"
    plist.write_text("old", encoding="utf-8")

    def fake_run(cmd, check=False, timeout=None):
        calls.append({"cmd": cmd, "check": check, "timeout": timeout})
        return SimpleNamespace(returncode=5)

    with patch.object(g, "get_launchd_plist_path", return_value=plist), \
         patch.object(g, "launchd_plist_is_current", return_value=False), \
         patch.object(g, "generate_launchd_plist", return_value="new"), \
         patch.object(g, "get_launchd_label", return_value="com.hermes.agent"), \
         patch.object(g, "_launchd_domain", return_value="gui/501"), \
         patch("subprocess.run", side_effect=fake_run), \
         patch("builtins.print", side_effect=lambda *a, **k: printed.append(" ".join(str(x) for x in a))):
        result = g.refresh_launchd_plist_if_needed()

print(result)   # actual: True
print(printed)  # actual: success message still emitted

Expected behavior

  • A failed bootout/bootstrap should surface as an error or a false return value.
  • The install/repair command should not claim success when the reload failed.

Actual behavior

  • Reload failures are swallowed and reported as success.

Suggested investigation

  • Check launchctl return codes (or use check=True with targeted handling for harmless bootout cases).
  • Add a regression test covering non-zero bootstrap return codes.

extent analysis

TL;DR

The refresh_launchd_plist_if_needed function should be modified to handle non-zero return codes from launchctl commands to accurately report failures.

Guidance

  • Modify the refresh_launchd_plist_if_needed function to set check=True when running launchctl commands to raise an exception on non-zero return codes.
  • Add targeted exception handling for harmless bootout cases to prevent false positives.
  • Implement a regression test in tests/hermes_cli/test_gateway_service.py to cover non-zero bootstrap return codes.
  • Review the hermes_cli/gateway.py code to ensure that error handling is properly propagated to the install and repair commands.

Example

try:
    subprocess.run(["launchctl", "bootout", "..."], check=True)
except subprocess.CalledProcessError as e:
    # Handle harmless bootout cases or re-raise the exception
    if e.returncode == 5:  # Example harmless return code
        print("Harmless bootout error")
    else:
        raise

Notes

The suggested changes assume that the subprocess module is used to run launchctl commands. The actual implementation may vary depending on the specific code and requirements.

Recommendation

Apply workaround: Modify the refresh_launchd_plist_if_needed function to handle non-zero return codes and add regression testing to ensure the fix is effective. This approach addresses the root cause of the issue and provides a clear path to resolving the problem.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

  • A failed bootout/bootstrap should surface as an error or a false return value.
  • The install/repair command should not claim success when the reload failed.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING