From 623f04720c474d803aa8f7c545c67dcb3a380931 Mon Sep 17 00:00:00 2001 From: majorlinux Date: Sun, 21 Jun 2026 13:00:35 -0400 Subject: [PATCH] Add macOS guide: auditing & cleaning Background App Activity (sfltool dumpbtm) --- ...s-background-app-activity-audit-sfltool.md | 154 ++++++++++++++++++ SUMMARY.md | 1 + 2 files changed, 155 insertions(+) create mode 100644 05-troubleshooting/macos-background-app-activity-audit-sfltool.md diff --git a/05-troubleshooting/macos-background-app-activity-audit-sfltool.md b/05-troubleshooting/macos-background-app-activity-audit-sfltool.md new file mode 100644 index 0000000..84ade36 --- /dev/null +++ b/05-troubleshooting/macos-background-app-activity-audit-sfltool.md @@ -0,0 +1,154 @@ +--- +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. diff --git a/SUMMARY.md b/SUMMARY.md index 689c50a..9c9ce90 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -127,6 +127,7 @@ updated: 2026-06-21T11:46 * [rsync over Tailscale: Hung in TCP Teardown After Transfer Completes](05-troubleshooting/networking/rsync-tailscale-teardown-stall.md) * [iOS Tailscale Clients Report HostName="localhost" — Breaks /etc/hosts Generators](05-troubleshooting/networking/tailscale-status-json-hostname-localhost-ios.md) * [macOS: Repeating Alert Tone from Mirrored iPhone Notification](05-troubleshooting/macos-mirrored-notification-alert-loop.md) + * [Auditing & Cleaning macOS Background App Activity (sfltool dumpbtm)](05-troubleshooting/macos-background-app-activity-audit-sfltool.md) * [Time Machine: Orphaned APFS `.previous` Folder Blocks All Backups](05-troubleshooting/time-machine-apfs-orphaned-previous-blocks-backup.md) * [OBS Studio: Stale Script Paths After Windows Profile Rename](05-troubleshooting/obs-stale-script-paths-after-windows-profile-rename.md) * [ClamAV CPU Spike: Safe Scheduling with nice/ionice](05-troubleshooting/security/clamscan-cpu-spike-nice-ionice.md)