PrefWatch v.1.2.0
1.2.0 — 2026-04-17
Feature
- New
--hot-domains <list>CLI flag / Jamf$10parameter: comma-separated list of domains kept permanently "active" so their first change is detected without waiting for the fs_usage→poll round-trip. Defaults:com.apple.finder,.GlobalPreferences. PassNONEto disable. - Contextual
# NOTE:for FinderPreviewPaneSettings— first-open writes the full attribute list; only subsequent toggles reflect actual user modifications. - README: new Security section flagging that
/var/log/prefwatch-v*.logmay contain user-specific data.
Fix
start_watchsingle-domain mode missed any change that happened within the same second as its initial baseline:stat -f %mhas 1-second granularity, so thecurrent_mtime != last_mtimecheck never fired for same-second writes and the change remained invisible until a later-second write occurred. Now forces a fullshow_domain_diffevery 4 poll iterations (~2s) regardless of mtime;defaults exportreadscfprefsddirectly and catches same-second writes the mtime check missed.- Detect Finder column view settings (
StandardViewOptions:ColumnViewOptions) — previously filtered out. - Finder NOTE clarified: View Options (Cmd+J) require 'Use as Defaults' for detection (icon/list view); column view writes directly.
Performance
- Preemptive
cfprefsdflush infs_watch: spawndefaults read $domin the background before invokingshow_plist_diff, giving the retry loop a better chance of seeing up-to-date data. - New active-domains registry:
fs_watchandpoll_watchmark each touched domain in$PREFWATCH_TMPDIR/active-domains/; every poll iteration pre-flushescfprefsdfor those domains in parallel viadefaults read. Non-destructive alternative tokillall cfprefsd. - HOT_DOMAINS are seeded into the active-domains registry at startup and auto-refreshed each poll cycle so they never expire — first change detected near-instantly.
- Tighter
show_plist_diffretry schedule0.1 0.2 0.3 0.5 0.7(total 1.8s) replacing the previous0.5 + 1.5(2s). More frequent flush hints catchcfprefsdsync faster in the fs_usage path. show_plist_diffretry loop skipsdump_plistwhen file mtime is unchanged (up to 5× per event saved). A final unconditional dump on the last retry ensures same-second writes are still caught, sincestat -f %mhas 1-second granularity.- Skip
dump_plist_jsonduring retries inshow_plist_diff— JSON is only needed after a change is confirmed. - Reduce
poll_watchcadence from 1s to 0.5s — halves the worst-case latency when the fs_usage path misses a flush. - Debounce
fs_usageevents per-plist at 300ms using$EPOCHREALTIME(fork-free viazsh/datetime).cfprefsdemits multiple fs_usage events per logical write (observable viafs_usage); collapsing them avoids redundantshow_plist_diffpasses. - Force line-buffered I/O in the fs_watch pipeline (
sed -l+awk fflush()) — prevents block buffering betweensed/awk/while readfrom holding back events on quiet pipelines. - Run the 3 Python diff invocations (
emit_array_additions,emit_array_deletionsvia new_py_deletions_rawhelper,emit_nested_dict_changes) in parallel in bothshow_plist_diffandshow_domain_diff. - Reduce forks in
_log(called for every output line): usezsh/datetimestrftimeinstead of/bin/dateinget_timestamp; replace twoprintf | sedpipelines with zsh[[ =~ ]]+${match[1]}capture ondefaults writemessages.
Noise
- Exclude
com.apple.metrickitd(MetricKit daemon —MXClient*keys are per-app diagnostic bookkeeping touched whenever Outlook, Teams, Edge, etc. query MetricKit, not user preferences) - Exclude
com.apple.imessage.bag(Apple service config bag:CacheTimeTTL +Daterefresh timestamp, server-controlled) - Filter
LastIMDNotificationPostedDateforcom.apple.iChat(Instant Messaging Daemon notification timestamp) - Filter
*lastAppUpdateCheck*|*LastAppUpdateCheck*globally (auto-update check timestamps, e.g. Raycastraycast-updates-lastAppUpdateCheckDate) - Filter
*lastProcessed*|*LastProcessed*(processing timestamps likelastProcessedDate) - Filter
*LastBackup*|*lastBackup*(TimeMachine daemon state likeLastBackupActivity) - Filter
CloudKitAccountInfoCache|*CloudKitAccountInfo*globally (hash-keyed CloudKit account cache) - Filter TimeMachine
Destinations:N:daemon sub-keys (BytesAvailable,BytesUsed,NumberOfSnapshots,SnapshotDates,ConsistencyScanDate,FilesystemTypeName,LastKnownEncryptionState,LastKnownVolumeName,ReferenceLocalSnapshotDate,attemptDate,backupOfVolumeUUIDs) — keeps user config (ID,Kind,QuotaGB,Name) - Filter
PreviewOptionsWindow.Locationfor Finder (Cmd+J panel window position)
Prevent masking real user preferences
Narrowed overly-broad global patterns in is_noisy_key() that could hide real prefs:
*History*→*HistoryItems*|*HistoryMetadata*|*HistoryList*|NSRecentDocumentsHistory|*HistoryDatabase*(keepsHistoryAgeInDaysLimit,EnableHistory)*Cache*→*CacheData*|*CachedBy*|*CacheVersion*|*CacheKey*|*CacheEntry*(keepsCacheSize,EnableCache,DiskCacheSize)*Temp*→ removed (kept*-temp|*-tmp|*TempFile*|*TempPath*) — avoid catchingTemplate*,ColorTemperature*ViewOptions*→*ViewOptionsFrame*|*ViewOptionsWindow*(suffix-specific window state)FK_SidebarWidth*→ removed (real user preference)*Analytics*|*Telemetry*→*AnalyticsQueue/Session/Event*|*TelemetryQueue/Session/Event*(keepsAnalyticsEnabled,SendAnalytics)SUSendProfileInfo→ removed (Sparkle opt-in toggle)flagsexact → removed (too generic)uses/*donate*→ removed/narrowed tolaunchCount,*donateDialogShown*,*lastDonateDate*state|status|State|Statusexact → removed*ConnectionState*→ removed*Date|Dateexact suffix → removed (specific date noise already covered)last-selection→ removed from global- ALL_CAPS regex → removed (could catch real prefs like
SHOW_HIDDEN_FILES)
Narrowed DEFAULT_EXCLUSIONS that hid entire domains with real user preferences:
com.apple.SoftwareUpdate→ removed (AutomaticDownload,AutomaticallyInstallMacOSUpdates,AutomaticCheckEnabled)com.apple.TimeMachine→ narrowed to.helper/.agent(keepsAutoBackup,ExcludedPaths)com.apple.security*→ narrowed to known daemon sub-domains