majorwiki/05-troubleshooting/macos-background-app-activity-audit-sfltool.md

7.3 KiB

title domain category tags status created updated
Auditing & Cleaning macOS Background App Activity (sfltool dumpbtm) troubleshooting general
macos
background-tasks
btm
sfltool
login-items
system-extensions
uninstall
little-snitch
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

  • Dispositionenabled = 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:

  1. Absolute paths are stored as file:// URLs, not plain /…. Strip the file:// prefix and URL-decode (%20 → space).
  2. Child items store a relative URL (e.g. Contents/Library/LoginItems/…) that must be joined to the parent record's absolute path, found via Parent 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) and developer (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 uninstall subcommand — 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 share UBF8T346G9.com.microsoft.oneauth, …entrabroker, and …teams across 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 rm alone can leave a registered system extension behind.