Skip to content

security(approval): close launchctl / hermes CLI self-termination gaps - #7817

Closed
BrownBear127 wants to merge 1 commit into
NousResearch:mainfrom
BrownBear127:security/launchctl-self-termination
Closed

security(approval): close launchctl / hermes CLI self-termination gaps#7817
BrownBear127 wants to merge 1 commit into
NousResearch:mainfrom
BrownBear127:security/launchctl-self-termination

Conversation

@BrownBear127

Copy link
Copy Markdown

Three additional DANGEROUS_PATTERNS entries closing self-termination vectors
not covered by the existing pkill/killall name-based pattern or the
pgrep-expansion patterns from #aedf6c79.

What

  1. macOS launchctl service controllaunchctl kickstart -k <target>
    sends SIGTERM to the target service. If the agent targets its own gateway
    service, launchd KeepAlive (SuccessfulExit: false) respawns the process,
    the resumed session sees a pending tool call with exit 130, and the model
    retries the same command. Result: infinite self-kill loop.

    Covers launchctl kickstart, kill, bootout, bootstrap, plus
    launchctl load/unload when the argument references a hermes service.

  2. hermes CLI gateway controlhermes -p <profile> gateway restart/stop/kill is the same class of self-termination when invoked
    from inside the gateway.

    Note: #a55c044c already addresses this at the CLI layer by detecting
    gateway ancestry and routing hermes gateway restart to a SIGUSR1 graceful
    restart. This PR adds defense-in-depth at the approval layer so that:

    • The raw pattern is flagged before it reaches the CLI path
    • stop/kill subcommands (which the CLI path doesn't turn into
      graceful restart) are also caught

Why — motivating incident

On 2026-04-11, an ade profile agent on macOS added a new Discord slash
command and included this step in its todo plan:

Restart ade Discord gateway so slash commands resync

The agent then executed:

launchctl kickstart -k gui/$(id -u)/ai.hermes.gateway-ade

launchctl sent SIGTERM to the running gateway. launchd respawned the gateway
(KeepAlive.SuccessfulExit = false). The new gateway loaded the session from
SQLite, saw the pending launchctl tool call with exit 130, and the codex model
decided to retry — reasoning (not unreasonably) that the command had been
interrupted and should be tried again.

This ran in a tight loop for ~14 hours. Each human message that arrived during
the loop was persisted into the session, giving the resumed model additional
context, but the model consistently returned to the same todo step and ran the
same command. The existing DANGEROUS_PATTERNS did not catch launchctl kickstart (not a pkill/killall) or hermes gateway restart (doesn't
match the hermes|gateway|cli.py name-in-body pattern in a pkill/killall
context).

The incident is a cousin of the one that motivated #aedf6c79 —
same "self-termination" category, different primitive.

Why not systemctl

systemctl --user restart hermes-gateway is intentionally not added to
DANGEROUS_PATTERNS. The existing test test_systemctl_restart_not_flagged
treats it as the supported Linux restart path, and #a55c044c already handles
self-invocation via SIGUSR1 at the CLI layer for systemctl. Adding a
DANGEROUS_PATTERNS entry would require removing that test, which would be
a behavior change rather than a pure gap fix.

If maintainers disagree and would prefer systemctl restart hermes-* to be
flagged symmetric with launchctl, I'm happy to add the pattern + update the
existing test in a follow-up commit.

Tests

12 new cases in TestGatewayProtection, following the same positive + negative
pattern as TestPgrepKillExpansion:

  • 7 launchctl cases: kickstart, kickstart with $(id -u), kill,
    bootout, unload+hermes, list (negative), print (negative)
  • 5 hermes-CLI cases: restart, stop, kill, start (negative),
    status (negative)

Full suite: 131 passed, 0 failed (119 before → 131 after).

Checklist

  • pytest tests/tools/test_approval.py passes
  • No changes to existing tests or patterns
  • Commit message follows #aedf6c79 format
  • Real-world incident referenced with concrete command
  • Defense-in-depth rationale explained (does not duplicate #a55c044c)

Three additional DANGEROUS_PATTERNS entries closing self-termination
vectors not covered by the existing pkill/killall name-based pattern or
the pgrep-expansion patterns from #<aedf6c79>.

1. **macOS launchctl service control** — `launchctl kickstart -k` sends
   SIGTERM to the target service. If the agent targets its own gateway
   service, launchd KeepAlive (`SuccessfulExit: false`) respawns the
   process, the resumed session sees a pending tool call with exit 130,
   and the model retries the same command. Result: infinite self-kill
   loop. Covers kickstart/kill/bootout/bootstrap and load/unload when
   targeting a hermes service.

2. **hermes CLI gateway control** — `hermes -p <profile> gateway
   restart/stop/kill` is the same class of self-termination when invoked
   from inside the gateway. Upstream #<a55c044c> already routes this to
   SIGUSR1 graceful restart at the CLI layer by detecting gateway
   ancestry, but defense-in-depth at the approval layer catches the raw
   pattern before it reaches that path — and also catches `stop`/`kill`
   subcommands that graceful restart does not.

Motivating incident (2026-04-11):

The ade profile ran `launchctl kickstart -k gui/$(id -u)/ai.hermes.
gateway-ade` as part of a todo plan to sync a new slash command.
Launchd restarted the process, resume saw the pending launchctl call,
the model retried, loop for ~14 hours until a human noticed. The
command bypassed all existing patterns because macOS `launchctl` is
not covered and `hermes gateway restart` (which the model also tried)
was not in the pkill/killall pattern.

Tests: 12 new cases in TestGatewayProtection covering positive and
negative paths:

  - 7 launchctl cases (kickstart, kickstart with $(id -u), kill,
    bootout, unload+hermes, list negative, print negative)
  - 5 hermes-CLI cases (restart, stop, kill, start negative, status
    negative)

`systemctl --user restart hermes-gateway` is intentionally NOT added —
the existing `test_systemctl_restart_not_flagged` treats it as the
supported Linux restart path, and the CLI-layer fix in #<a55c044c>
already handles self-invocation via SIGUSR1.

Full suite: 131 passed, 0 failed.
@BrownBear127

Copy link
Copy Markdown
Author

Thanks for addressing both architectural gaps in #9895 and #9941 — the stuck-loop detection in particular is a much stronger fix than pattern matching alone.

#9895 covers hermes gateway stop/restart, hermes update, and systemctl restart, but the macOS launchctl primitives (kickstart, kill, bootout, bootstrap, load/unload) aren't included yet. In the original incident, the codex model bypassed the hermes CLI wrapper entirely and ran raw launchctl kickstart -k gui/$(id -u)/ai.hermes.gateway-ade — which is why pattern-level coverage for launchctl specifically matters.

This PR adds those macOS patterns + 12 tests. Happy to rebase onto current main if there are conflicts after #9895. Also happy to close if you'd prefer to fold these into a follow-up.

@alt-glitch alt-glitch added type/security Security vulnerability or hardening P1 High — major feature broken, no workaround tool/terminal Terminal execution and process management comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint labels Apr 29, 2026
@egilewski

Copy link
Copy Markdown
Contributor

merge conflicts

This PR does not merge cleanly with the base branch. Please rebase or merge current main and resolve the conflicts if it's still relevant.

Signed: GPT-5.5-medium in Codex

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jun 29, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for this, and for the detailed incident writeup — the 14-hour ade-profile self-kill loop is exactly the kind of thing we want guarded.

Closing in favor of #55515 (merged: #55515), which addresses the one genuine residual gap your report surfaced.

A couple of notes on why we narrowed the scope:

  • The launchctl half is already fully covered on main by Dangerous command approval can be bypassed by sending SIGTERM to gateway process #33071 (launchctl stop/kickstart/bootout/unload/kill/disable/remove targeting a hermes/ai.hermes label). I ran all 7 of your launchctl cases against current main and every one already behaves correctly, including the $(id -u) form and the negative list/print cases.
  • hermes gateway kill is not an actual subcommand (the real verbs are run/start/stop/restart/status/install/uninstall/list/setup/migrate-legacy/enroll), so that pattern would never match a real self-termination.
  • The real gap was that the existing hermes-CLI pattern required hermes and gateway to be adjacent, so a profile flag (hermes -p ade gateway restart — your incident's exact form) slipped past it. fix(approval): catch hermes gateway stop/restart behind a profile flag #55515 fixes precisely that by allowing global flags between hermes and the gateway subcommand, and adds tests for the profile-flag forms.

Appreciate the contribution — it pointed straight at the bug.

@teknium1 teknium1 closed this Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P1 High — major feature broken, no workaround sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/terminal Terminal execution and process management type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants