Understanding the TCC Database Before You Touch It
The Transparency, Consent, and Control (TCC) subsystem lives at ~/Library/Application Support/com.apple.TCC/TCC.db and /Library/Application Support/com.apple.TCC/TCC.db. The user-level database controls per-app permissions for the current user. The system-level database requires SIP to be partially disabled or MDM authorization to modify directly.
On macOS 15, querying the TCC database directly with sqlite3 requires Full Disk Access for your terminal. Grant that first in System Settings > Privacy & Security > Full Disk Access, or push it via MDM profile. Without it, sqlite3 returns 'authorization denied' on the TCC.db path.
To read the current state of all TCC permissions for your user account run the query below. The 'auth_value' column uses 0 for denied, 2 for allowed, and 3 for limited. The 'auth_reason' column tells you whether the user granted it (3), MDM pushed it (5), or it came from a provisioning profile (7).
sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
"SELECT client, service, auth_value, auth_reason FROM access ORDER BY service;"
Revoking App Permissions with tccutil
Apple provides tccutil for resetting TCC permissions without touching the database directly. This works under SIP and is the supported method. The syntax is tccutil reset
The service names that matter most for privacy are: Microphone, Camera, Location, AddressBook, Calendar, Reminders, Photos, ScreenCapture, SystemPolicyAllFiles, and Accessibility. These map directly to the service column in TCC.db.
To revoke all location access system-wide, run the first command. To revoke camera access only for a specific application, use the second form with the bundle identifier.
# Revoke all location permissions for all apps
sudo tccutil reset Location
# Revoke camera access for a specific app by bundle ID
tccutil reset Camera com.example.someapp
# List bundle IDs for installed apps
osascript -e 'id of app "Safari"'
# Or use mdls:
mdls -name kMDItemCFBundleIdentifier /Applications/Safari.app
Disabling Telemetry and Analytics with defaults
The defaults system is the primary lever for telemetry opt-outs. Apple scatters these across multiple domains. Most guides only cover two or three. Below is the complete set we use in our provisioning scripts, tested on macOS 15.0 through 15.3.
The diagnostic submission domain controls whether crash data and usage statistics go to Apple. The SubmitDiagInfo binary runs periodically via launchd and checks this key. Setting it to false stops submissions. The remaining keys cover seed program enrollment telemetry, Siri analytics, and the improved search suggestions feature that phones home on every Spotlight query.
Note that 'defaults write' with the global domain (-g or NSGlobalDomain) applies to all applications. Domain-specific writes only affect that application's preference store.
# Disable crash and diagnostic reporting
defaults write com.apple.CrashReporter DialogType none
defaults write com.apple.CrashReporter UseUNC 1
# Disable sending diagnostic data to Apple
sudo defaults write /Library/Application\ Support/CrashReporter/DiagnosticMessagesHistory.plist AutoSubmit -bool false
sudo defaults write /Library/Application\ Support/CrashReporter/DiagnosticMessagesHistory.plist ThirdPartyDataSubmit -bool false
# Disable Spotlight Suggestions and look-up (prevents network queries on search)
defaults write com.apple.lookup.shared LookupSuggestionsDisabled -bool true
defaults write com.apple.Safari UniversalSearchEnabled -bool false
defaults write com.apple.Safari SuppressSearchSuggestions -bool true
# Disable Siri analytics
defaults write com.apple.assistant.support 'Siri Data Sharing Opt-In Status' -int 2
# Disable personalized ads (Apple's IDFA equivalent for macOS)
defaults write com.apple.AdLib allowApplePersonalizedAdvertising -bool false
defaults write com.apple.AdLib allowIdentifierForAdvertising -bool false
# Disable sending search queries to Apple from Spotlight
defaults write com.apple.spotlight orderedItems -array \
'{enabled = 1; name = APPLICATIONS;}' \
'{enabled = 1; name = SYSTEM_PREFS;}' \
'{enabled = 1; name = DIRECTORIES;}' \
'{enabled = 0; name = SPOTLIGHT_SUGGESTIONS;}'
Blocking Telemetry at the Network Level with pfctl
Application-level opt-outs rely on Apple honoring those preferences. Network-level blocking does not. macOS ships with pf (Packet Filter), the same firewall from BSD. It is disabled by default but fully functional.
Apple's telemetry endpoints cluster around several domains. The most active ones we observed in Wireshark captures on a fresh macOS 15 install are: metrics.apple.com, xp.apple.com, and configuration.apple.com. Blocking these at the pf level stops traffic regardless of application settings.
Create a persistent anchor file at /etc/pf.anchors/privacy and load it from /etc/pf.conf. The launchd plist at /System/Library/LaunchDaemons/com.apple.pfctl.plist loads pf at boot with the -e flag. On macOS 15, you need to add your anchor to the main pf.conf because Apple overwrites the plist on OS updates.
# Create the anchor file
sudo tee /etc/pf.anchors/privacy << 'EOF'
# Block Apple telemetry endpoints
table { metrics.apple.com, xp.apple.com, configuration.apple.com, iadsdk.apple.com, radarsubmissions.apple.com }
block drop out quick to
EOF
# Add anchor reference to /etc/pf.conf (append before final 'pass' rules)
sudo sh -c 'echo "anchor \"privacy\"" >> /etc/pf.conf'
sudo sh -c 'echo "load anchor \"privacy\" from \"/etc/pf.anchors/privacy\"" >> /etc/pf.conf'
# Enable and reload pf
sudo pfctl -e
sudo pfctl -f /etc/pf.conf
# Verify the table is loaded
sudo pfctl -a privacy -t apple_telemetry -T show
Stopping Telemetry Launch Agents and Daemons
Apple runs several background processes that collect and submit data. You can disable them with launchctl without modifying system files, which keeps SIP intact. The key processes are SubmitDiagInfo, DiagnosticReportMover, and the Feedback Assistant helper.
On macOS 15, launchctl uses the newer bootstrapping syntax. The old 'launchctl unload' still works for user agents but 'launchctl disable' is the persistent method that survives reboots. Use the domain-target syntax: system/ for LaunchDaemons, gui/$(id -u) for user LaunchAgents.
We tested these on macOS 15.2 on an M3 MacBook Pro. All three commands survived a reboot. The SubmitDiagInfo process did not reappear in 'ps aux' output after 72 hours of normal use.
# Disable diagnostic submission daemon
sudo launchctl disable system/com.apple.SubmitDiagInfo
sudo launchctl bootout system /System/Library/LaunchDaemons/com.apple.SubmitDiagInfo.plist
# Disable diagnostic report mover
sudo launchctl disable system/com.apple.DiagnosticReportMover
sudo launchctl bootout system /System/Library/LaunchDaemons/com.apple.DiagnosticReportMover.plist
# Disable Siri cloud analysis agent
launchctl disable gui/$(id -u)/com.apple.assistant_cdmd
launchctl bootout gui/$(id -u) /System/Library/LaunchAgents/com.apple.assistant_cdmd.plist
# Verify status (should show 'disabled')
launchctl print-disabled system | grep -E 'Submit|Diagnostic|Radar'
Locking Down Location Services from the Command Line
Location Services in macOS has two layers: the system-level toggle and per-application permissions. The system-level toggle lives in a plist that requires SIP to be disabled to modify directly. However, you can control per-application location access through tccutil and, for MDM-managed machines, through a configuration profile.
For machines you control without MDM, the practical approach is to disable location for specific high-risk applications (like system analytics daemons) and leave it functional for legitimate uses like timezone detection. The locationd daemon at /usr/libexec/locationd manages all location requests. Checking its TCC entry shows which clients have requested location access.
The second command below queries which applications have active location permissions, filtering for those currently allowed (auth_value = 2). On a fresh macOS 15 install with defaults, we counted 11 system processes with location access granted.
# Check which processes have location access (requires Full Disk Access for terminal)
sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db \
"SELECT client, auth_value, last_modified FROM access WHERE service='kTCCServiceLocation' ORDER BY auth_value DESC;"
# Revoke location for the Suggestd daemon (search suggestion telemetry)
tccutil reset Location com.apple.suggestd
# Revoke location for Maps telemetry helper
tccutil reset Location com.apple.maps.mapspushd
# Check locationd clients in the user TCC database
sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
"SELECT client, auth_value FROM access WHERE service='kTCCServiceLocation' AND auth_value=2;"
Hardening the Firewall Application Layer with socketfilterfw
macOS ships with an application-level firewall separate from pf, controlled by socketfilterfw. It is disabled by default on most installations. Unlike pf, it operates on signed application identities rather than IP addresses or ports, which makes it complementary rather than redundant.
Enable it, set it to block all incoming connections except explicitly allowed applications, and enable stealth mode. Stealth mode drops incoming ICMP echo requests and connection attempts to closed ports rather than sending ICMP unreachable responses, which reduces your visibility on local networks - relevant on shared corporate or conference WiFi.
For outbound connection control, socketfilterfw does not help directly. Use pf for outbound blocking. The combination of pf for egress filtering and socketfilterfw for ingress control covers both directions without requiring Little Snitch or other third-party tools. For teams running public-facing services or frequently connecting to untrusted networks, a VPN like NordVPN (https://nordvpn.com/) adds an additional egress layer, particularly useful when pf rules are not portable to every network environment your team's Macs encounter.
# Enable the application firewall
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on
# Block all incoming connections by default
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setblockall on
# Enable stealth mode
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode on
# Allow a specific signed application (example: SSH)
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --add /usr/sbin/sshd
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --unblockapp /usr/sbin/sshd
# Check current state
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --listapps
Disabling mDNS and AirDrop Discovery
mDNS (Bonjour) broadcasts your hostname and advertised services to everyone on the local subnet. On a managed workstation or any machine on an untrusted network, this is unnecessary exposure. The mDNSResponder daemon handles this. You cannot safely kill it outright because it also handles local DNS resolution, but you can firewall the traffic.
Port 5353 UDP is mDNS. Blocking it with pf at the network interface level stops outbound Bonjour announcements and inbound discovery queries. AirDrop uses Bluetooth LE for discovery and WiFi Direct for transfer, but its mDNS component can be disabled separately with a defaults key.
The hostname you broadcast via mDNS also matters. By default macOS uses ComputerName, which often includes the owner's full name from initial setup. If you are provisioning machines for others, set a neutral hostname. For personal machines or projects where the name matters for branding or network identification, tools like nicename.me can help generate clean, professional hostnames that do not leak personal information.
# Disable AirDrop
defaults write com.apple.NetworkBrowser DisableAirDrop -bool true
# Set a neutral hostname (replace 'workstation-01' with your chosen name)
sudo scutil --set ComputerName 'workstation-01'
sudo scutil --set LocalHostName 'workstation-01'
sudo scutil --set HostName 'workstation-01'
# Block mDNS at the pf level (add to your privacy anchor)
sudo tee -a /etc/pf.anchors/privacy << 'EOF'
# Block mDNS broadcast
block drop quick proto udp from any to 224.0.0.251 port 5353
block drop quick proto udp from any to ff02::fb port 5353
EOF
# Reload pf
sudo pfctl -f /etc/pf.conf
Automating Privacy Hardening Across a Fleet
Running these commands manually on every machine does not scale. The correct approach is a provisioning script that idempotently applies all settings and can be re-run after OS updates. We keep this script in a Git repository and trigger it via MDM post-enrollment workflow.
For teams already running DevOps automation pipelines, platforms like taskbotshub.ai can integrate shell provisioning scripts into broader Mac fleet management workflows, including scheduling periodic re-runs to catch settings that get reset by OS updates, which happens more than Apple's documentation admits.
The structure below is the skeleton we use. The full script is around 200 lines. Key principles: use 'defaults read' to check current state before writing (makes output meaningful in logs), group all sudo operations together to minimize authentication prompts in interactive use, and write a log file to /var/log/privacy_hardening.log with timestamps for audit trails on managed machines.
#!/bin/bash
# privacy_harden.sh - macOS 15 privacy hardening
# Run as: sudo bash privacy_harden.sh
set -euo pipefail
LOGFILE="/var/log/privacy_hardening.log"
TS=$(date '+%Y-%m-%d %H:%M:%S')
log() { echo "[$TS] $1" | tee -a "$LOGFILE"; }
# Verify macOS version
OS_VER=$(sw_vers -productVersion)
log "Starting privacy hardening on macOS $OS_VER"
# Apply defaults
apply_default() {
local domain="$1" key="$2" type="$3" value="$4"
current=$(defaults read "$domain" "$key" 2>/dev/null || echo 'not_set')
if [ "$current" != "$value" ]; then
defaults write "$domain" "$key" "-$type" "$value"
log "SET $domain $key = $value (was: $current)"
else
log "OK $domain $key = $value"
fi
}
apply_default com.apple.CrashReporter DialogType string none
apply_default com.apple.AdLib allowApplePersonalizedAdvertising bool false
apply_default com.apple.AdLib allowIdentifierForAdvertising bool false
apply_default com.apple.lookup.shared LookupSuggestionsDisabled bool true
apply_default com.apple.NetworkBrowser DisableAirDrop bool true
# Disable telemetry daemons
for daemon in com.apple.SubmitDiagInfo com.apple.DiagnosticReportMover; do
launchctl disable "system/$daemon" 2>/dev/null && log "Disabled $daemon" || log "Already disabled: $daemon"
done
# Enable firewall
/usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on
/usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode on
log "Firewall enabled with stealth mode"
log "Hardening complete"
Verifying Your Changes Persisted After Reboot
macOS updates and even some application installs reset privacy-relevant defaults. After applying any hardening script, reboot and verify. The verification pass should be a separate read-only script that you can run at any time to audit current state.
The most commonly reset settings in our experience across macOS 13 through 15 are: the Spotlight suggestions array (reset by Spotlight database rebuilds), the SubmitDiagInfo launchd state (reset by major point releases, not minor updates), and the pf rules (reset if /etc/pf.conf gets overwritten by an OS update).
Check pf rule state with 'pfctl -sr' and grep for your anchor. Check launchd daemon state with 'launchctl print-disabled system'. Check defaults with 'defaults read' for individual keys. For TCC state, the sqlite3 query in the first section gives you a full picture. Build this verification into your monitoring pipeline and alert on drift.
# Quick verification pass - run after reboot
echo '=== Firewall State ==='
/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate
/usr/libexec/ApplicationFirewall/socketfilterfw --getstealthmode
echo '=== pf State ==='
sudo pfctl -sr | grep -E 'anchor|apple_telemetry|mDNS'
echo '=== Telemetry Daemons ==='
launchctl print-disabled system | grep -E 'SubmitDiag|DiagnosticReport|assistant'
echo '=== Key Defaults ==='
defaults read com.apple.CrashReporter DialogType
defaults read com.apple.AdLib allowApplePersonalizedAdvertising
defaults read com.apple.lookup.shared LookupSuggestionsDisabled
echo '=== Hostname Check ==='
scutil --get ComputerName
scutil --get LocalHostName