---
title: "The Helper Was Registered. Its Lifetime Outlived the App."
description: "A macOS login item, launch agent, or launch daemon can remain active after the visible app exits. A defensible review separates packaging, registration, approval, launch domain, runtime authority, updates, failure recovery, and removal."
date: 2026-09-06
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/posts/macos-service-management-lifecycle/
---

## The macOS helper lifecycle in 60 seconds

A visible macOS application may close while its login item, launch agent, or launch daemon continues
to run. That behavior can be correct: a sync client needs background work, a security product needs a
system extension, and an updater may need a narrowly privileged service. The security boundary is not
the existence of a helper. It is the relationship between who packaged it, who registered it, who
approved it, where it runs, what it accepts, how it updates, and how it stops.

Modern Service Management APIs let an application control helper executables kept inside its signed
bundle. Apple's [`SMAppService`](https://developer.apple.com/documentation/servicemanagement/smappservice)
documentation distinguishes login items, launch agents, and launch daemons and exposes registration,
unregistration, and status. Registration means the service can begin launching subject to the rules
and approval for that service type. It does not mean that every IPC client is authorized or that the
runtime effect matches the user's expectation.

The useful pentest question is:

> Which authority survives when the app window, user session, version, account, or product itself is
> no longer active?

Answering it requires lifecycle evidence, not a screenshot of Login Items and not a broad dump of the
personal Mac's background services.

## Packaging, registration, approval, and execution are separate states

Four statements that sound similar have different security meanings:

1. **Packaged:** a helper executable and property list are present inside a signed application bundle.
2. **Registered:** the application asked Service Management to make the helper launchable.
3. **Approved:** the person or administrator allowed the relevant background or system-level item.
4. **Running:** `launchd` created a process in a specific domain, user context, and session.

Apple documents that in macOS 13 and later, `SMAppService` is the supported control for bundled login
items, launch agents, and launch daemons. The details differ. A login item can start immediately and
at subsequent logins. A registered launch agent may bootstrap in the user's session. A launch daemon
does not bootstrap until administrator approval and can return on subsequent boots. The [registration
documentation](https://developer.apple.com/documentation/servicemanagement/smappservice/register%28%29)
is explicit that registration is subject to user approval.

These transitions should become a state machine in the assessment:

<figure class="diagram">
<svg viewBox="0 0 720 330" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A macOS helper moves through packaged, registered, approved, launched, updated and removed states while failure paths return to a bounded disabled state.">
<defs><marker id="msml-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker></defs>
<rect x="18" y="24" width="126" height="52" rx="8" class="dg-box"/><text x="32" y="45" class="dg-label">packaged</text><text x="32" y="62" class="dg-muted">SIGNED BUNDLE</text>
<line x1="146" y1="50" x2="176" y2="50" class="dg-line" marker-end="url(#msml-a)"/>
<rect x="180" y="24" width="128" height="52" rx="8" class="dg-box"/><text x="194" y="45" class="dg-label">registered</text><text x="194" y="62" class="dg-muted">SERVICE RECORD</text>
<line x1="310" y1="50" x2="340" y2="50" class="dg-line" marker-end="url(#msml-a)"/>
<rect x="344" y="24" width="128" height="52" rx="8" class="dg-box-accent"/><text x="358" y="45" class="dg-label">approved</text><text x="358" y="62" class="dg-accent">USER · ADMIN</text>
<line x1="474" y1="50" x2="504" y2="50" class="dg-line" marker-end="url(#msml-a)"/>
<rect x="508" y="24" width="194" height="52" rx="8" class="dg-box-accent"/><text x="522" y="45" class="dg-label">launched</text><text x="522" y="62" class="dg-accent">DOMAIN · UID · SESSION</text>
<line x1="605" y1="78" x2="605" y2="116" class="dg-line" marker-end="url(#msml-a)"/>
<rect x="508" y="120" width="194" height="54" rx="8" class="dg-box"/><text x="522" y="142" class="dg-label">runtime operation</text><text x="522" y="160" class="dg-muted">PEER · METHOD · EFFECT</text>
<line x1="506" y1="147" x2="474" y2="147" class="dg-line" marker-end="url(#msml-a)"/>
<rect x="344" y="120" width="128" height="54" rx="8" class="dg-box"/><text x="358" y="142" class="dg-label">updated</text><text x="358" y="160" class="dg-muted">NEW IDENTITY</text>
<line x1="342" y1="147" x2="310" y2="147" class="dg-line" marker-end="url(#msml-a)"/>
<rect x="180" y="120" width="128" height="54" rx="8" class="dg-box-accent"/><text x="194" y="142" class="dg-label">unregistered</text><text x="194" y="160" class="dg-accent">NO RELAUNCH</text>
<line x1="244" y1="176" x2="244" y2="214" class="dg-line" marker-end="url(#msml-a)"/>
<rect x="180" y="218" width="128" height="54" rx="8" class="dg-box"/><text x="194" y="240" class="dg-label">removed</text><text x="194" y="258" class="dg-muted">STATE CLEANED</text>
<rect x="410" y="218" width="184" height="54" rx="8" class="dg-box-danger"/><text x="424" y="240" class="dg-label">failure or denial</text><text x="424" y="258" class="dg-danger">DISABLED · VISIBLE</text>
<line x1="508" y1="76" x2="502" y2="214" class="dg-line" marker-end="url(#msml-a)"/>
<text x="18" y="312" class="dg-crit">LIFECYCLE IS AUTHORITY</text><text x="210" y="312" class="dg-mono">install and uninstall are security transitions</text>
</svg>
<figcaption>The helper is not simply present or absent. Every transition changes who can launch it, which identity runs, and what remains after failure or removal.</figcaption>
</figure>

## Start from the supplied bundle, not the whole laptop

A privacy-safe review inventories one authorized product. It does not enumerate every personal login
item, agent, daemon, process, or application installed on the assessor's Mac.

For the named app artifact, record:

- each helper executable and its bundle-relative location;
- the enclosing bundle's signature and each helper's own designated requirement;
- the service property-list name, label, program path, arguments, and launch conditions;
- login item, user agent, system daemon, or system extension role;
- registration and approval state for the product under test;
- launch domain, UID, session availability, sandbox, and entitlements;
- XPC or other message interfaces and their peer requirements;
- update, rollback, uninstall, and orphan cleanup paths.

Read-only inspection can start with the customer-supplied artifact:

```bash
find CustomerApp.app/Contents/Library -maxdepth 3 \
  \( -name 'LaunchAgents' -o -name 'LaunchDaemons' -o -name 'LoginItems' \) -print
codesign -dvvv --entitlements :- CustomerApp.app
plutil -p CustomerApp.app/Contents/Library/LaunchAgents/com.example.agent.plist
```

Repeat signature inspection for the actual helper executable. Do not infer helper identity from the
outer app's marketing name or Team ID alone. Two correctly signed components can have different
identifiers, entitlements, sandbox state, and update behavior.

Apple's migration guidance recommends keeping supported helper resources and their property lists in
the signed app bundle. That reduces mutable installation material, but it does not remove the need to
validate the service configuration or the runtime interface.

## Model launch domains and authority explicitly

A login item, launch agent, and launch daemon are not interchangeable persistence labels.

| Component           | Typical lifetime                      | Context                      | Important security question                                               |
| ------------------- | ------------------------------------- | ---------------------------- | ------------------------------------------------------------------------- |
| Login item          | login and user session                | logged-in user               | Does the visible feature need to start every login?                       |
| Launch agent        | user session, on demand or persistent | per-user launch domain       | Which same-session clients can reach its interface?                       |
| Launch daemon       | boot or on demand                     | system domain, commonly root | Which authenticated client can turn requests into system effects?         |
| System extension    | system-managed lifecycle              | restricted extension context | Which entitlement, approval, event source, and containing app control it? |
| Bundled XPC service | normally client-coupled               | separate process             | Does process separation also enforce peer and method authorization?       |

Record the effective property-list semantics instead of assigning a risk score to `RunAtLoad` alone.
Demand-driven services may still be persistent in practice if a public Mach service repeatedly wakes
them. A process that exits cleanly may be relaunched after failure. Conversely, a daemon that exists
on disk but is disabled and unreachable does not prove active impact.

The authority graph should connect **launch trigger → runtime principal → callable interface → system
effect**. A root label without a reachable method is not the finding; a user agent with broad privacy
access and an unauthenticated interface may be more consequential.

## Registration does not authorize the XPC caller

Service Management decides whether and how the helper can launch. XPC decides how processes exchange
messages. The receiver still owns peer authentication, operation authorization, input validation, and
effect verification.

For every callable method, ask:

1. Which code requirement must the connecting process satisfy?
2. Is that identity allowed to invoke this method in the current product and user state?
3. Which arguments are chosen by the caller?
4. Does the service resolve final files and objects safely in its own context?
5. Can a lower-authority client request a higher-authority effect?
6. What audit record distinguishes request, decision, outcome, and actual effect?

The synthetic XPC lab used in part one demonstrates this boundary without touching a system daemon.
A temporary user-domain service accepted the same harmless input from two differently signed clients.
The server admitted the expected signing identifier and rejected the mismatch before method execution,
then the service was removed. That is a testing primitive, not evidence that a third-party product is
vulnerable.

<figure class="diagram">
<svg viewBox="0 0 720 326" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A helper lifecycle test compares approved and mismatched clients across app exit, login, update, denial and uninstall checkpoints while verifying service and state effects.">
<defs><marker id="msml-b" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker></defs>
<rect x="18" y="22" width="148" height="54" rx="8" class="dg-box-accent"/><text x="32" y="44" class="dg-label">registered helper</text><text x="32" y="62" class="dg-accent">TEST DOMAIN</text>
<line x1="168" y1="49" x2="206" y2="49" class="dg-line" marker-end="url(#msml-b)"/>
<rect x="210" y="22" width="152" height="54" rx="8" class="dg-box"/><text x="224" y="44" class="dg-label">approved peer</text><text x="224" y="62" class="dg-muted">ALLOW METHOD</text>
<line x1="364" y1="49" x2="402" y2="49" class="dg-line" marker-end="url(#msml-b)"/>
<rect x="406" y="22" width="184" height="54" rx="8" class="dg-box-accent"/><text x="420" y="44" class="dg-label">bounded effect</text><text x="420" y="62" class="dg-accent">SYNTHETIC MARKER</text>
<rect x="210" y="112" width="152" height="54" rx="8" class="dg-box-danger"/><text x="224" y="134" class="dg-label">mismatched peer</text><text x="224" y="152" class="dg-danger">DENY BEFORE CALL</text>
<rect x="406" y="112" width="184" height="54" rx="8" class="dg-box"/><text x="420" y="134" class="dg-label">app exits</text><text x="420" y="152" class="dg-muted">EXPECTED LIFETIME</text>
<rect x="210" y="202" width="152" height="54" rx="8" class="dg-box"/><text x="224" y="224" class="dg-label">update or denial</text><text x="224" y="242" class="dg-muted">SAFE FAILURE</text>
<rect x="406" y="202" width="184" height="54" rx="8" class="dg-box-accent"/><text x="420" y="224" class="dg-label">unregister</text><text x="420" y="242" class="dg-accent">NO RELAUNCH · CLEAN</text>
<line x1="92" y1="78" x2="92" y2="229" class="dg-line"/><line x1="92" y1="139" x2="206" y2="139" class="dg-line" marker-end="url(#msml-b)"/><line x1="92" y1="229" x2="206" y2="229" class="dg-line" marker-end="url(#msml-b)"/><line x1="364" y1="139" x2="402" y2="139" class="dg-line" marker-end="url(#msml-b)"/><line x1="364" y1="229" x2="402" y2="229" class="dg-line" marker-end="url(#msml-b)"/>
<text x="18" y="309" class="dg-crit">RETEST THE TRANSITIONS</text><text x="210" y="309" class="dg-mono">not only the first successful launch</text>
</svg>
<figcaption>A lifecycle test proves the intended run path, the rejected peer, behavior after app exit or update, and complete unregistration.</figcaption>
</figure>

## Update and rollback are identity transitions

An update can replace the containing app, helper executable, service property list, protocol version,
and code requirement. Test it as a security migration.

Important cases include:

- old app with old helper;
- new app with new helper;
- new app encountering a still-running old helper;
- interrupted update between bundle replacement and service transition;
- rollback to an older protocol or signature requirement;
- app moved to another path after registration;
- helper crash during migration;
- revoked or disabled background permission during upgrade.

Fail closed without creating an unrecoverable system state. A new client should not silently weaken its
peer requirement to talk to any leftover process. An old helper should not accept new operations it
cannot validate. The updater should authenticate both source and destination artifacts and preserve a
clear rollback boundary.

Version the XPC protocol and make unsupported combinations explicit. Tie high-impact operations to the
current installed product state rather than accepting any binary ever signed by the same developer.
Developer identity is often too broad for method authorization; the designated requirement should
name the intended product and any permitted migration window.

## Uninstall is a security feature

Deleting the visible `.app` is not proof that every helper stopped or that its state disappeared. The
product needs a supported removal path that unregisters services, stops background work, invalidates
credentials, and handles data according to the user's retention choice.

Verify removal at four layers:

- **registration:** Service Management reports that the product's helper is no longer registered;
- **runtime:** the named service cannot relaunch and the IPC name is unreachable;
- **artifact:** product-owned helper files and mutable configuration are removed as documented;
- **authority:** tokens, bookmarks, privileged approvals, queues, and remote registrations no longer
  produce effects.

Do not use a broad `launchctl` or filesystem dump as proof. Query the exact product label or test-domain
identifier. This is both more reliable and more respectful of the device owner's privacy.

## Evidence matrix

| Claim                                       | Positive evidence                                                  | Negative control                                        | What remains unproven                               |
| ------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------- | --------------------------------------------------- |
| Helper is packaged as claimed               | Bundle-relative path, per-executable signature, property-list hash | Altered copy fails signature verification               | Runtime approval on another Mac                     |
| Registration creates the intended service   | Exact `SMAppService` status and named service response             | Unregistered state cannot launch                        | Legacy installer paths not supplied for review      |
| Runtime identity matches design             | Named launch domain, UID, entitlements, protocol version           | Wrong session or wrong peer denied                      | All enterprise management combinations              |
| High-authority methods authenticate callers | Approved code requirement reaches one synthetic effect             | Mismatched signing identifier denied before method body | Unknown future signed products from same developer  |
| Update preserves the boundary               | Old-to-new and interrupted-update cases have explicit outcomes     | Unsupported client/helper pair fails closed             | Vendor update infrastructure outside scope          |
| Uninstall removes effective persistence     | Service unregistered, cannot relaunch, product state cleaned       | Restart or login does not recreate marker               | Data intentionally retained under documented policy |

Evidence should distinguish configuration, observation, and effect. A property list shows desired
configuration. A process listing shows an observation at one instant. A bounded test proves whether a
specific client can cause a specific result. None of those alone proves the entire lifecycle.

## Remediation priorities

For most findings, repair the narrowest failed transition:

- use the current Service Management model instead of mutable installer scripts where supported;
- keep helper artifacts inside the signed bundle and verify each executable identity;
- request registration when the background feature is explained, not as unrelated setup noise;
- authenticate XPC peers with a narrow code requirement and authorize every method separately;
- minimize daemon operations and pass handles or structured intent instead of arbitrary commands;
- version protocols and reject unsafe old/new combinations;
- make disabled, denied, and crash states visible without coercive re-registration loops;
- log lifecycle decisions with product-scoped identifiers and no personal content;
- provide complete unregister and removal behavior, then regression-test it across login and reboot.

Persistence is not automatically malicious, and user approval is not automatically sufficient. A
high-quality macOS product makes the helper's purpose, authority, current state, and removal as clear
as the visible application itself.

## Sources

- Apple Developer Documentation, [Service Management](https://developer.apple.com/documentation/servicemanagement/)
- Apple Developer Documentation, [`SMAppService`](https://developer.apple.com/documentation/servicemanagement/smappservice)
- Apple Developer Documentation, [`register()`](https://developer.apple.com/documentation/servicemanagement/smappservice/register%28%29)
- Apple Developer Documentation, [Updating helper executables from earlier versions of macOS](https://developer.apple.com/documentation/servicemanagement/updating-helper-executables-from-earlier-versions-of-macos)
- Apple Developer Documentation, [Updating your app package installer to use the new Service Management API](https://developer.apple.com/documentation/servicemanagement/updating-your-app-package-installer-to-use-the-new-service-management-api)
- Apple Developer Documentation, [XPC](https://developer.apple.com/documentation/xpc)

This is a source-reviewed assessment methodology. Its local example uses a temporary user-domain
service and synthetic input; it does not enumerate a personal Mac's background items or claim a
third-party persistence vulnerability.