A macOS monitoring tool that watches preference changes in real-time and generates the exact commands to reproduce them.
- Reproducible commands. Every change is emitted as the exact command that recreates it:
defaults/PlistBuddy, or the right built-in CLI (networksetup,scselect,scutil,systemsetup,sharing,tmutil,nvram,spctl/socketfilterfw,mdutil,bioutil,pmset,lpadmin,lpoptions,cupsctl,launchctl,kickstart,dscl), or apython3one-liner where macOS offers no CLI at all - ALL mode. Watch every domain at once; no need to know which one changed
- Contextual notes. Inline
# NOTE:comments: how to apply a change, the tool whendefaultscan't, or why it isn't reproducible (see Scope) - ByHost support. Emits
-currentHostfor per-hardware prefs (trackpad, Bluetooth) - Noise filtering. 500+ rules, so only real changes surface
- Minimal dependencies. One zsh script + Python 3
Run in Terminal. Output is also logged and viewable in Console.app.
# Monitor all preferences
sudo ./prefwatch.sh
# Monitor a specific domain (no sudo, lower CPU)
./prefwatch.sh com.apple.finder
# Verbose mode
sudo ./prefwatch.sh -v
# Stop it
sudo pkill -f 'prefwatch\.sh'| Option | Short | Description | Default |
|---|---|---|---|
[domain] |
-- | Domain to monitor (no sudo needed) | ALL (sudo) |
--verbose |
-v |
Show diffs and debug info | Off |
--debug |
-- | Log # FILTERED: <dom> <key> (reason) when a detected change is suppressed (answers "why didn't my change appear?") |
Off |
--log <path> |
-l |
Custom log file path | Auto |
--no-system |
-- | Exclude /Library/Preferences |
Include |
--exclude <glob> |
-e |
Domain patterns to exclude in ALL mode; naming a domain explicitly always watches it | Built-in |
--hot-domains <list> |
-- | Comma-separated domains kept permanently active for instant first-change detection (pass NONE to disable) |
common System Settings panels (see HOT_DOMAINS) |
--mdm |
-- | Make emitted commands fleet-deployable from a root Jamf policy: user-domain commands are prefixed with a runAsUser helper, PlistBuddy paths use $loggedInUser/$UUID (ByHost) |
Off |
--no-console |
-- | Don't open Console.app or stop when it closes. Run until Ctrl+C (interactive/VM testing) | Off |
--fs-usage |
-- | ALL mode as root: also run the fs_usage real-time detector next to polling (Jamf $12) |
Off |
Jamf reserves $1–$3 (mount point, computer name, user), so PrefWatch takes its parameters from $4 onward: $4=domain, $5=log path, $6=include system, $7=only cmds, $8=exclusions, $9=MDM output, $10=hot domains, $11=debug, $12=fs_usage. Launches Console.app for live viewing; logs to stdout + file + syslog.
PrefWatch reproduces what lands in a watched plist (defaults/PlistBuddy), plus the out-of-band settings its CLIs cover (above).
A few changes it detects but can't reduce to one built-in command. It emits an explanatory # NOTE: instead: FileVault (needs a recovery key), the battery charge limit (SMC-managed), a new user account, a Dock reorder, Media Sharing, and a privacy permission (a PPPC profile, not a command). Where an install-first helper reproduces it, the NOTE names the tool (see Third-party tools).
Everything else is invisible. No output is expected, not a bug: internal app databases (Safari, Mail, Calendar), sandboxed app prefs (App Store apps keep theirs under ~/Library/Containers), and hardware state (display & keyboard brightness, HDR).
A # NOTE: also rides on a reproduced change: how to apply it (logout/login, killall, restart a service, run as root), or a caveat on the emitted command. A positional array index or a ByHost/display UUID that won't transplant, or a pane that writes every default on first open.
For settings with no built-in command, a # NOTE: names the tool. And for default apps and the wallpaper it emits the tool's command outright, with the real value:
utiluti. Default apps (URL schemes & file types)dockutil. Dock items and orderdesktoppr. Desktop wallpaper, and the colour behind it
- ALL mode without
sudocovers~/Library/Preferences. Root is what adds/Library/Preferences, the sharing commands and launchd state. Full Disk Access is what names a privacy permission; without it the change is reported, not named. - Detection is by polling, so latency depends on when
cfprefsdflushes writes to disk. Hot domains are flushed every 0.5s so changes surface in a second or two; a cold domain can take about ten seconds on its first change. Pass it via--hot-domainsupfront if that matters.
PrefWatch logs plist diffs to /var/log/prefwatch-v*.log and syslog. These may contain user-specific data (IDs, tokens, paths, the privacy-permission table). The log is 0600, owned by the console user. Review before sharing. Use --exclude to skip sensitive domains.
MIT. See LICENSE.