--- title: "Auditing & Cleaning macOS Background App Activity (sfltool dumpbtm)" domain: troubleshooting category: general tags: [macos, background-tasks, btm, sfltool, login-items, system-extensions, uninstall, little-snitch] status: published created: 2026-06-21 updated: 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 ```bash 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: 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: ```python 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) ``` ```bash 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. ```bash 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 ```bash 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: ```bash ls /Library/LaunchDaemons/* /Library/LaunchAgents/* 2>/dev/null ls -d "/Library/Application Support/" 2>/dev/null ls ~/Library/Preferences/* 2>/dev/null ``` Then boot out the daemon and remove the files: ```bash sudo launchctl bootout system /Library/LaunchDaemons/.daemon.plist 2>/dev/null sudo rm -f /Library/LaunchDaemons/.daemon.plist /Library/LaunchAgents/.agent.plist sudo rm -rf "/Library/Application Support/" "$HOME/.Trash/.app" rm -f ~/Library/Preferences/*.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 ```bash 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 # 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.