btm_add event — and on the trust verdict that the kernel foundation stamps on the process that did the installing.
Findings are produced on-device and appear in Malware, Frameworks, and related Security views. The sections below describe the persistence signal itself. The rest of this page covers what is specific to persistence.
Why btm_add is the highest-signal primitive on macOS
On Linux, persistence is a scavenger hunt: cron, systemd units, rc files, shell profiles, LD_PRELOAD, and a dozen more, each a separate place to watch and a separate parser to maintain. On macOS 13+ (Ventura and later), Apple funnels essentially all of it through one framework — Background Task Management (BTM) — and the collector subscribes to its NOTIFY_BTM_LAUNCH_ITEM_ADD callback. When anything registers a launch agent, launch daemon, or login item, a single btm_add row lands in endpoint_events.
That gives this page two structural advantages:
- It fires at registration, not at run. You see the persistence the moment it is installed — before the next reboot, before the payload ever executes a second time. There is no window to win a race in.
- It is never filtered. Persistence events bypass volume filtering entirely. A
btm_addis always recorded, regardless of who installed it, so the page never has a blind spot created by a noise rule.
btm_add does not just say “a launch daemon was added” — it says which process added it, signed by whom, trusted how. That last part is the entire story of severity on this page.
The btm_add payload
Beyond the always-present code-signing and application attribution columns, a persistence event carries three payload fields:
These three, plus the installing process’s trust verdict, are everything the page reasons about.
A
launch_daemon is the most powerful of the three because it runs as root before any user logs in. When the timeline shows a daemon registered by a binary you do not trust, that is the row to open first.Page-specific structure
The shared layout’s generic pieces take on a persistence-specific shape.The “Persistence items” activity group
The page has one activity group, Persistence items, and its summary card shows the count of registered launch items and the worst severity among them. On the reference Mac that card reads ~1,389 persistence findings — which sounds enormous until you see the dedup, because the overwhelming majority are a handful of well-signed updaters re-registering the same plist over and over.Severity is the installer’s trust verdict
This is the headline insight of the page, and the cleanest illustration of the whole section’s thesis: the same action gets a different severity depending entirely on who did it.
A launch agent registered by a
developer_id updater is info and scrolls past. The same launch agent registered by an unsigned_or_invalid or adhoc binary is high and sits at the top of the table. You are not writing path-based allow/deny rules — the trust tier on the installing process is the severity input, and it comes for free on every row.
Dedup: the chatty-updater problem
Legitimate software re-registers its persistence constantly. Docker Desktop, for instance, rewrites its launch-agent plist roughly once a minute, which would otherwise bury the table under hundreds of identicaldeveloper_id rows. The shared grouped table collapses near-identical findings — same rule, same installing app, same item_path — into one row with an N× badge you can expand to see every timestamp. So Docker’s minute-by-minute churn reads as a single Docker ×1,000+ line, and the ~1,389 raw findings resolve to a short, legible list of distinct persistence items.
Finding detail
Selecting a row opens the standard six-part detail panel. On Persistence each panel is filled out like this:
The Process responsible panel is the one that matters most here. The
team_id tells you the exact Apple Developer team that signed the installer (empty, correctly, for Apple platform binaries), and the cdhash is a cryptographic fingerprint you can pivot on across the whole table to find every other thing that binary did.
Querying persistence directly
Becausebtm_add carries the installer’s trust verdict on the same row, the high-severity slice of this entire page is one WHERE clause:
code_signature filter to developer_id and you get the routine, info-level baseline — the updaters and helpers that make up most of the ~1,389 findings. The verdict column is the only thing that changes between “show me the noise” and “show me the foothold.”
btm_add requires macOS 13.0 (Ventura) or later — BTM is the framework that backs it. On older hosts the subscription degrades gracefully: the rest of the sensor keeps running, this page simply has no rows to show.How it fits the rest of the section
Persistence is a pinpoint detector — it tells you precisely what was installed and by whom. Two neighbors give it context:- Process Lineage answers the next question. When an
unsigned_or_invalidbinary registers a launch daemon, you want to know what spawned that binary. Theresponsible_pidandcdhashon the finding pivot straight into theexec/forktree to reconstruct the kill chain back to its origin. - ATT&CK Matrix rolls these findings up. Every persistence finding lights its mapped cell — T1543.001, T1543.004, T1547.011, or T1053.003 — so a single launch-daemon install shows up both here, as a row, and there, as coverage of the Persistence tactic column.
btm_add payload and event coverage. For why trust and identity ride on every row in the first place, start at the foundation.