7.3 KiB
| title | domain | category | tags | status | created | updated | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Auditing & Cleaning macOS Background App Activity (sfltool dumpbtm) | troubleshooting | general |
|
published | 2026-06-21 | 2026-06-21 |
Auditing & Cleaning macOS Background App Activity (sfltool dumpbtm)
Overview
macOS tracks every login item, agent, daemon, helper, and extension that may run in the background in its Background Task Management (BTM) database. The GUI shows this under System Settings → General → Login Items & Extensions ("Allow in the Background"), but the GUI is summarised and hides paths, identifiers, and orphans.
sfltool dumpbtm prints the full BTM database from the command line — and the per-user records need no sudo. This is the fastest way to answer "what is allowed to run in the background, and does each entry still map to an installed app?"
List what's registered
sfltool dumpbtm # per-user records, no sudo required
Each record looks like:
Name: CleanMyMac Menu
Type: login item (0x4)
Disposition: [enabled, allowed, notified] (0xb)
Identifier: 4.com.macpaw.CleanMyMac-mas.Menu
URL: Contents/Library/LoginItems/CleanMyMac_5_MAS_Menu.app
Bundle Identifier: com.macpaw.CleanMyMac-mas.Menu
Parent Identifier: 2.com.macpaw.CleanMyMac-mas
Reading the fields
- Disposition —
enabled= actively allowed to run in the background.disabled= present but off. - Type — what kind of item it is:
| Type | Meaning |
|---|---|
app (0x2) |
A normal application entry |
login item (0x4) |
Launches at login (menu-bar apps, helpers) |
agent (0x8) / legacy agent |
Per-user background agent |
legacy daemon (0x10010) |
System-wide background daemon |
background tasks (0x2000) |
Abstract background-task registration owned by a parent app — has no file path of its own |
developer (0x20) |
A per-developer grouping header (the collapsible row in Settings), not an app |
quicklook / spotlight / dock tile |
Plugins/extensions — not really "background apps" |
Map entries to installed apps (find orphans)
Two gotchas make naïve path-checking fail:
- Absolute paths are stored as
file://URLs, not plain/…. Strip thefile://prefix and URL-decode (%20→ space). - Child items store a relative
URL(e.g.Contents/Library/LoginItems/…) that must be joined to the parent record's absolute path, found viaParent Identifier.
A small parser that resolves each record to a real path and flags true orphans:
import sys, re, os, urllib.parse
items, cur = [], None
def push():
global cur
if cur is not None: items.append(cur)
for line in sys.stdin:
s = line.strip()
if re.match(r"^#\d+:$", s): push(); cur = {}; continue
if cur is None: continue
m = re.match(r"^([A-Za-z][A-Za-z /]+):\s*(.*)$", s)
if m: cur[m.group(1).strip()] = m.group(2).strip()
push()
byid = {it["Identifier"]: it for it in items if it.get("Identifier")}
def abspath(it, d=0):
if d > 8: return None
u = it.get("URL", "")
if u and u != "(null)":
if u.startswith("file://"): return urllib.parse.unquote(u[7:]).rstrip("/")
if u.startswith("/"): return u.rstrip("/")
par = byid.get(it.get("Parent Identifier", ""))
if par:
b = abspath(par, d + 1)
if b: return os.path.join(b, urllib.parse.unquote(u)).rstrip("/")
return None
for it in items:
if not it.get("Name"): continue
p = abspath(it)
if p and not os.path.exists(p):
print("ORPHAN:", it["Name"], "->", p)
sfltool dumpbtm | python3 btm_check.py
Expected non-orphans:
background tasks (0x2000)anddeveloper (0x20)rows legitimately store no path — they are not missing apps. Helpers/daemons that resolve inside a parent bundle (e.g./Applications/Foo.app/Contents/Library/LoginItems/…) or in/Library/…are also fine; they just don't appear as a top-level.app. That is usually why an entry "has no application you can find."
Disable background for an app
This cannot be scripted — Apple deliberately gates the toggle behind the GUI:
System Settings → General → Login Items & Extensions → "Allow in the Background" → switch the app off.
Disabling a developer (0x20) grouping header turns off all of that developer's sub-items at once.
Uninstall cleanly — the system-extension trap
Dragging an app to the Trash is not a full uninstall. Apps that install a network/system extension plus a privileged daemon (firewalls and VPNs especially — Little Snitch, Mullvad, etc.) leave their /Library daemon still loaded and running after the app is trashed. The BTM entry persists and the background service keeps working.
1. Prefer the app's own uninstaller
- Bundled uninstall script (Mullvad): runs cleanly, deactivates the system extension, resets the firewall.
sudo "/Applications/Mullvad VPN.app/Contents/Resources/uninstall.sh" - Some apps ship an uninstaller in their DMG or a CLI tool. Note: Little Snitch 6.x has no DMG uninstaller and no
littlesnitch uninstallsubcommand — manual removal is the supported route there.
2. Check whether a system extension is still active
systemextensionsctl list
If the app's extension is not listed (only unrelated ones like Tailscale/Canon remain), the extension is already deactivated and a manual file removal is now complete and safe.
3. Manual removal (when no uninstaller exists)
Find every component first:
ls /Library/LaunchDaemons/<id>* /Library/LaunchAgents/<id>* 2>/dev/null
ls -d "/Library/Application Support/<Vendor>" 2>/dev/null
ls ~/Library/Preferences/<id>* 2>/dev/null
Then boot out the daemon and remove the files:
sudo launchctl bootout system /Library/LaunchDaemons/<id>.daemon.plist 2>/dev/null
sudo rm -f /Library/LaunchDaemons/<id>.daemon.plist /Library/LaunchAgents/<id>.agent.plist
sudo rm -rf "/Library/Application Support/<Vendor>" "$HOME/.Trash/<App>.app"
rm -f ~/Library/Preferences/<id>*.plist # user-owned, no sudo
Shared-container caution: before deleting
~/Library/Group Containers/*, check it isn't shared. Microsoft apps shareUBF8T346G9.com.microsoft.oneauth,…entrabroker, and…teamsacross Office/Teams/RDP — delete only the app-specific container (e.g.…com.microsoft.rdc), never the shared auth ones.
Stale BTM "ghost" entries
After a manual uninstall, sfltool dumpbtm may still list the removed app, pointing at now-deleted paths. These are harmless orphans (nothing left to load). BTM reconciles them on the next reboot / login cycle — a reboot also finalises any system-extension teardown.
Quick reference
sfltool dumpbtm # full per-user BTM dump (no sudo)
sfltool dumpbtm | grep -A6 'Name:' # browse records
systemextensionsctl list # active network/system extensions
# Verify a removal:
sfltool dumpbtm | grep -i <vendor> # should be empty after a reboot
See also
- Apple gates "Allow in the Background" behind System Settings — there is no supported CLI toggle for BTM dispositions.
- For VPN/firewall apps, always reach for the vendor uninstaller first; manual
rmalone can leave a registered system extension behind.