Mac Health Check 5.0.1 corrects Palo Alto Networks GlobalProtect VPN detection; 5.0.0 added a dedicated AI skill for easier Mac Admin customization, historical Memory Pressure warnings, targeted post-remediation verification, Jamf Pro clock-skew detection and macOS 27 compatibility improvements
Mac Health Check provides a practical, MDM-agnostic, user-friendly approach to surfacing Mac compliance information directly to end-users via an MDM's Self Service.
Built using the open-source utility swiftDialog, the solution acts as a “heads-up display” that presents real-time system health and policy compliance status in a clear and interactive format.
Deployment of Mac Health Check involves configuring organizational defaults, embedding the script in your MDM, creating a policy to run it on demand and testing to ensure proper output and behavior.
Administrators can customize the user interface using swiftDialog’s visual capabilities, making the experience both informative and approachable.

Rocketman Tech December 2025 Meetup (05-Dec-2025)
Mac Health Check is particularly valuable in IT support workflows, serving as an initial triage point for Tier 1 support by confirming network access, credentials, and MDM connectivity, while also acting as a verification tool for Tier 2 teams both during and after remediation efforts.
The tool logs results for review, writes a structured JSON health report locally, can optionally forward that report to Splunk HEC, and does not remediate or enforce device configuration. It does make a few deliberate local changes: outside Silent, it installs or updates swiftDialog when it is missing, reports no version, or is older than 3.1.1.4997 (unless it already matches the latest production release) and quits other running swiftDialog windows; it installs the Client-Side Cache copy and LaunchDaemon, sets the client log (/var/log/org.churchofjesuschrist.log) to root:admin mode 640, appends a root-only Memory Pressure history sample, clears organizationDefaultsDomain before each Jamf Pro external check, briefly enables ipconfig verbose mode to read the Wi-Fi SSID (restored only when it changed it), and the sample CrowdStrike external check temporarily sets AppleLocale to en_US (restored on exit). In Self Service, Mac Health Check launches a detached swiftDialog Inspect Mode preset6 guided summary built from finalized results plus a live compliance plist for swiftDialog 3.1.1.4997 compliance findings. When a valid full-run baseline less than 36 hours old contains warnings, failures, or errors, the next Self Service run automatically rechecks only those findings, merges the new results into the prior full report by stable check key, and records per-check timestamps. Healthy reruns within 15 minutes can still replay the cached summary without re-running health checks. Full Silent health-check runs generate the same Inspect Mode config and compliance plist artifacts without launching swiftDialog.
- Structured JSON health report with optional Splunk HEC delivery (see JSON / Splunk Reporting)
- Beginning in
5.0.0, persistent runtime state (canonical report and lock, SOFA andnetworkQualitycaches) lives in root-owned/Library/Management/org.churchofjesuschristinstead of world-writable/var/tmp, while user-facing Inspect assets (config and compliance plist) live in a root-owned0755tree at/Library/Application Support/org.churchofjesuschrist/Inspect, with per-user control files underInspect/Users/<user>; cached reports are trusted only when they are root-owned regular files, and root-owned pre-5.0.0leftovers in/var/tmpare removed automatically - Splunk HEC tokens and webhook URLs are passed to
curlthrough--config -on stdin, so they never appear in thecurlchild's arguments, andDebugmode'sset -xtracing starts after parameter parsing and is suppressed inside secret-handling functions - Beginning in
5.0.0, Splunk HEC and webhook deliveries requirehttps://URLs (curl --proto =https --tlsv1.2); anhttp://Parameter 7 HEC URL is logged as[ERROR]and nothing is sent.Silent+splunkOperationMode=productionruns whose only HEC token is a rejected Parameter 8 value exit1immediately, before discovery, to minimize the time the token spends in the process list - Slack and Microsoft Teams webhook messages are effectively Jamf Pro-only (with a
View in Jamf Prolink to the computer record); the Mosyle branch has no console URL yet, so its messages carry an empty link and may be rejected, and every other MDM skips webhook messages - Webhook deliveries fail on HTTP errors: Slack and Microsoft Teams responses other than
2xxare logged with their HTTP status,5xxand connection failures get up to three attempts in total, and a failed delivery is logged as a warning without changing report status or exit codes - Script Parameters 5 and 8 remain visible to any local user (via
ps) for as long as the root script runs, so beginning in5.0.0the script rejects a Splunk HEC token or webhook URL supplied only through those parameters (logged as[ERROR]; Splunk HEC delivery and webhook messages are skipped, soSilent+splunkOperationMode=productionexits1). Deploy them in the root-only secrets file/Library/Management/org.churchofjesuschrist/MacHealthCheck-Secrets.plist(keyssplunkHECTokenandwebhookURL;root:wheel, mode600, for example from a package payload) and leave Parameters 5 and 8 blank; setallowParameterSecrets="true"in the script only as a temporary, not-recommended legacy opt-in. Secrets-file values take precedence over the parameters, an untrusted secrets file is ignored with a warning, and each run logs which source was used. Rotate any token previously passed as a parameter, and restrict the HEC token to the Mac Health Check index and sourcetype - Parameter 11 forces a fresh run by bypassing
Self Servicetargeting/replay or theSilent+splunkOperationMode=productioncached upload (a forced freshSilentrun also removes the cached report before running all checks); see Script Parameters - Beginning in
4.0.0,Self Service,DebugandSilent+splunkOperationMode=productionruns (neverTestorDevelopment, and only when the running script is a root-owned file in root-controlled directories, such as the Jamf Pro script cache, the package payload path or a self-extracting wrapper's root-only directory under sticky/var/tmp) install a client-side copy at/Library/Management/org.churchofjesuschrist/MHC.zshplus aorg.churchofjesuschrist.MHCLaunchDaemon that refreshes the local report across a deterministic 00:53-01:53 window centered on 1:23 a.m. - The LaunchDaemon sets
launchDaemonRun=true; the client-side script then derives a stable per-Mac jitter from hardware UUID, logs the jitter through MHC-prefixed logging, and routes daemon stdout/stderr to/dev/nullto avoid duplicate client-log lines. - When a LaunchDaemon-triggered refresh runs with no active GUI user, Mac Health Check falls back to
/Library/Preferences/com.apple.loginwindow.plistlastUserNamefor user-scoped checks. Silent+splunkOperationMode=productionruns from any MDM refresh the client-side copy first, then upload the cached report without re-running checks when the client-side script version matches and/Library/Management/org.churchofjesuschrist/MacHealthCheck-Report.jsonis valid and less than 36 hours old
See: Resources/Splunk-Dashboard-Reference.md for the shipped Dashboard Studio JSON, its shared SPL pattern, and the 5.0.0 report field reference.
The inspectSummaryPreset is now an on / off toggle: on generates the Preset 6 inspect-summary assets, launches the summary in Self Service, and enables healthy-result cached replay; off disables asset generation, launch, and replay. Unresolved findings take precedence over replay and trigger targeted verification when the canonical report is eligible.
The current 5.0.1 release targets swiftDialog 3.1.1.4997 or newer so Self Service can use the PR #684 Preset 6 spacing and highlight refinements plus the JSON options block (moveable, ontop, minimise button) for the detached summary. Older compatible swiftDialog builds retain their prior visual treatment. PR #684 also tolerates quoted scalar values from MDM templating tools; Mac Health Check continues to emit native JSON numbers and booleans.
User-facing report:
dialog --inspect-mode --inspect-config "/Library/Application Support/org.churchofjesuschrist/Inspect/MacHealthCheck-Inspect-Config.json"Terminal summary of most recent health issues:
sudo jq -r '
"",
"Mac Health Check 5: Recent Warnings & Failures",
"",
"Hostname: \(.metadata.hostname) (\(.metadata.localHostName))",
"Timestamp: \(.metadata.timestamp)",
"Overall Status: \(.summary.overallStatus)",
"Healthy: \(.summary.healthyCount) | Warning: \(.summary.warningCount) | Fail: \(.summary.failCount) | Error: \(.summary.errorCount)",
"",
(.checks[]
| select(.status != "healthy")
| "- \(.name): \(if (.rawValue? | type) != "string" or .rawValue == "" then .message else .rawValue end)"),
""
' /Library/Management/org.churchofjesuschrist/MacHealthCheck-Report.json- User has a working Internet connection
- User knows their directory credentials
- Mac can execute policies
- Validates Network Access Controls
- Initial assessment for support sessions
- Easily confirms remediation efforts
- Provides peace-of-mind for end-users
- Silently performs all health checks and logs results
- No dialog is presented to the end-user
- Ideal for background compliance reporting
- Complements existing MDM compliance frameworks
- Full
Silenthealth-check runs generate the Inspect Mode config and compliance plist (see Inspect Mode Summary) without launching swiftDialog - When combined with
splunkOperationMode=production, suppresses non-Splunk stdout/stderr noise in Jamf policy logs while continuing to write the full run to${scriptLog} - In that same
Silent+splunkOperationMode=productioncombination,updateComputerInventory()logs a skip message and does not runjamf recon - Client-Side Cache uses a local LaunchDaemon copy to refresh
/Library/Management/org.churchofjesuschrist/MacHealthCheck-Report.jsonnightly; the nightly copy itself never transmits to Splunk or sends webhook messages, and the HEC token and webhook URL stay only in the root-onlyMacHealthCheck-Secrets.plist - LaunchDaemon-triggered refreshes use the active console user when present, and otherwise fall back to loginwindow
lastUserNamefor user-scoped checks - Any MDM can then run
Silent+splunkOperationMode=productionto upload the cached report only when the client and server script versions match
#!/bin/zsh --no-rcs
launchDaemonLabel="org.churchofjesuschrist.MHC"
launchDaemonPath="/Library/LaunchDaemons/${launchDaemonLabel}.plist"
organizationDirectory="/Library/Management/org.churchofjesuschrist"
inspectAssetsDirectory="/Library/Application Support/org.churchofjesuschrist/Inspect"
# Stop/unload daemon:
/bin/launchctl bootout system "${launchDaemonPath}" 2>/dev/null || true
/bin/launchctl disable "system/${launchDaemonLabel}" 2>/dev/null || true
# Remove Client-Side Cache assets:
/bin/rm -fv "${launchDaemonPath}"
# WARNING: also removes MacHealthCheck-Secrets.plist (Splunk HEC token / webhook URL),
# root-only Memory Pressure history and its lock file, and anything else your
# organization keeps in this directory; remove individual files instead if it is shared.
/bin/rm -rfv "${organizationDirectory}"
# Remove Inspect assets and per-user Inspect control files:
/bin/rm -rfv "${inspectAssetsDirectory}"
# Remove a Dock-named swiftDialog copy left by an interrupted run:
/bin/rm -rfv "/Library/Application Support/Dialog/Mac Health Check.app"
# (The report, SOFA cache and networkQuality cache all live under
# "${organizationDirectory}" and are removed above.)
# Optional pre-5.0.0 cached/report artifacts (5.0.0 removes root-owned leftovers automatically):
/bin/rm -fv /var/tmp/MacHealthCheck-Report.json
/bin/rm -rfv /var/tmp/MacHealthCheck-Report.lock
/bin/rm -fv /var/tmp/MacHealthCheck-Inspect-Config.json
/bin/rm -fv /var/tmp/MacHealthCheck-Inspect-Compliance.plist
/bin/rm -fv /var/tmp/MacHealthCheck-Inspect.trigger
/bin/rm -fv /var/tmp/MacHealthCheck-Inspect_final.trigger
/bin/rm -fv /var/tmp/MacHealthCheck-Inspect.ready
/bin/rm -fv /var/tmp/MacHealthCheck-Inspect-Result.json
/bin/rm -fv /var/tmp/MacHealthCheck-Inspect-Summary.log
/bin/rm -rfv /var/tmp/networkQualityTest
/bin/rm -rfv /var/tmp/sofa
/bin/rm -fv /var/tmp/dockicon.png
# Optional log removal:
/bin/rm -fv /var/log/org.churchofjesuschrist.log- When
enableDockIntegration="true"(default), non-Silentmodes launch swiftDialog with--showdockiconand--dockicon dockIconis configurable and supportsdefault, local paths,file://paths andhttp(s)URLs- Mac Health Check copies
Dialog.appto/Library/Application Support/Dialog/${humanReadableScriptName}.appand launchesdialogclifrom that bundle so Dock hover text matches the script name; only the run that created that copy removes it at exit, andSilentruns leave both the copy and/var/tmp/dialog.login place for any concurrent run dockiconbadgeshows the number of remaining checks, decreases after each completed check and is removed when checks complete- If dock icon setup fails, Mac Health Check logs a warning and falls back to
/Library/Application Support/Dialog/Dialog.app/Contents/MacOS/dialogcli - The copied bundle is re-signed ad hoc, which drops swiftDialog's Team ID; PPPC or notification configuration profiles keyed to that Team ID do not match the copy, so set
enableDockIntegration="false"where those profiles matter - Beginning in
5.0.0, the root script calls swiftDialog, Jamf Pro (/usr/local/jamf/bin/jamf) andjqthrough root-owned absolute paths and removes/usr/local/binfromPATH, because Homebrew can make that directory user-writable
| Parameter | Variable | Default | Description |
|---|---|---|---|
| 4 | operationMode |
Self Service |
Self Service, Silent, Debug, Development or Test (case-sensitive and not validated: any other value, such as silent or Developer, runs the full MDM check suite with the dialog, writes the canonical report and installs the Client-Side Cache, but skips Self Service-only targeted rechecks, cached replay and the Inspect summary, so double-check the spelling) |
| 5 | webhookURL |
(blank) | Microsoft Teams or Slack webhook URL (https:// only; effectively Jamf Pro-only); rejected unless allowParameterSecrets="true", so leave blank and use MacHealthCheck-Secrets.plist |
| 6 | splunkOperationMode |
test |
off disables HEC delivery, test writes the local JSON report without transmitting, production posts to Splunk HEC; case-insensitive, and any other value falls back to test with an [ERROR] |
| 7 | splunkHECURL |
(blank) | Splunk HTTP Event Collector URL (https:// only; an http:// URL is refused and logged as [ERROR]) |
| 8 | splunkHECToken |
(blank) | Splunk HEC token; rejected unless allowParameterSecrets="true", so leave blank and use MacHealthCheck-Secrets.plist |
| 9 | splunkHECIndex |
(blank) | Optional HEC index included in the transmission wrapper |
| 10 | splunkHECSourcetype |
(blank) | Optional HEC sourcetype included in the transmission wrapper |
| 11 | forceFreshRun |
false |
One-shot fresh run; bypasses Self Service targeted verification/replay and the Silent + splunkOperationMode=production cached upload |
Other settings, including externalCheckTimeoutSeconds (default 120; a Jamf Pro external-check policy that runs longer is terminated and reported as Timed Out), inspectReplayMaximumAgeSeconds (900), allowParameterSecrets (false) and enableDockIntegration (true), are source-level variables in Mac-Health-Check.zsh; see Diagrams/06-health-check-reference.md for the full list.
| Exit | When |
|---|---|
0 |
Every check is healthy, or only warnings were found (Computer Needs Attention); a healthy cached Inspect summary was replayed; a cached Splunk report upload succeeded |
1 |
At least one check failed (Computer Unhealthy); a Self Service targeted recheck could not lock, merge or write the canonical report; a pre-flight fatal (not root, insecure organizationDirectory, missing jq, swiftDialog download or install failure); swiftDialog Team ID verification failed; a cached Splunk report upload failed; Jamf Pro was detected but /private/var/log/jamf.log is missing; Silent + splunkOperationMode=production received only a rejected Parameter 8 HEC token |
Silent + splunkOperationMode=production is reporting-first: it exits 0 only when the local report is generated and Splunk HEC delivery succeeds with no reporting errors, regardless of health findings, and 1 otherwise. In other modes, a failed full-run report write is logged as a reporting error without changing the exit code, and webhook delivery failures never change it.
The MDM branch (list items, checks and webhook support) is chosen from the enrolled MDM server URL, matched case-sensitively in this order: addigy (Addigy), filewave (Filewave), fleet (Fleet), jamf or jss (Jamf Pro), jumpcloud (JumpCloud), kandji (Kandji / Iru), microsoft (Microsoft Intune) and mosyle (Mosyle). Any other URL, including Iru URLs that do not contain kandji and unenrolled Macs, runs the MDM-agnostic fallback (Unknown MDM vendor: None).
The following health checks and information reporting are included in version 5.0.1, which operates in Self Service mode by default. (Use Debug, Development or Test while testing; deploy with Self Service or Silent.)
Mac Health Check version
5.0.1corrects Palo Alto Networks GlobalProtect VPN detection when stale DEM keys reportDisconnectedwhile connected. Version5.0.0retains secure JSON report generation and optional Splunk HEC delivery, Client-Side Cache nightly report caching for cached Splunk uploads, Inspect Mode summary assets for swiftDialog3.1.1.4997PR #684 refinements,Quick Actions, a conditionalRemediation Guide, status-aware 12-point bento-grid spacing, fullSilentInspect asset generation without launching UI, healthy-result 15-minute cached summary replay,Wi-Fi Strength, and warning-only final dialog handling viaComputer Needs Attention, while adding targeted remediation rechecks, Jamf Pro clock skew detection, historical Memory Pressure warnings, and improved macOS 27 compatibility, Bluetooth Sharing, staged-update, uptime, and detached-summary behavior.
- macOS Version
- Available Updates (including deferred, staged, and DDM-enforced updates)
- System Integrity Protection
- Signed System Volume (SSV)
- Firewall
- FileVault Encryption
- Gatekeeper / XProtect
- Touch ID
- Password Hint (not run on Jamf Pro or Kandji)
- AirDrop
- AirPlay Receiver
- Bluetooth Sharing
- VPN Client
- Last Reboot
- Free Disk Space
- User's Directory Size and Item Count
- Desktop
- Downloads
- Trash
- MDM Profile
- Entra ID Registration**
- MDM Certificate Expiration
- Apple Push Notification service
- Mosyle Check-In***
- Jamf Pro Check-In**
- Jamf Pro Inventory**
- 🆕 Clock Skew**
- Extended Network Checks
- Apple Push Notification Hosts
- Apple Device Management
- Apple Software and Carrier Updates
- Apple Certificate Validation
- Apple Identity and Content Services
- Jamf Hosts**
- App Auto-Patch
- Homebrew Status
- Electron Corner Mask 🔗
- Organizationally required Applications (e.g., Microsoft Teams)
- BeyondTrust Privilege Management*
- Cisco Umbrella*
- CrowdStrike Falcon*
- Palo Alto GlobalProtect*
- Wi-Fi Strength
- 🆕 Memory Pressure
- Network Quality Test
- Computer Inventory**
* Requires external check (Jamf Pro only; these four sample checks run by default on Jamf Pro, so remove their list items and calls, or use the selector skill, if you have not deployed their policies; otherwise each reports Error and every run ends with Computer Needs Attention)
** Requires Jamf Pro
*** Requires Mosyle
Each MDM runs its own list-item array: Jamf Pro runs 42 items, Mosyle 34, Addigy, Fleet, JumpCloud and Microsoft Intune 33, Filewave and Kandji 32, and the MDM-agnostic fallback 29 (no MDM Profile, MDM Certificate Expiration or App Auto-Patch). Organizationally required applications vary by MDM (for example, Microsoft Teams, Fleet Desktop, Company Portal or Mosyle Self-Service), and Kandji also omits AirDrop, AirPlay Receiver, App Auto-Patch, MDM Profile, Homebrew Status and Electron Corner Mask.
Jamf Pro runs check Clock Skew with /usr/bin/sntp -n 1 -t 3 time.apple.com before inventory submission. The command queries one DNS record with a 3-second SNTP timeout inside the existing 5-second outer timeout. Offsets greater than 5 minutes are reported as a failure because they can prevent Jamf Pro inventory submission and other time-sensitive services from working correctly; when sntp cannot reach time.apple.com (for example, UDP 123 is blocked), the check reports Unable to determine as a warning.
Apple Push Notification service reads the last 24 hours of unified log entries from SSV-protected system processes only (paths under /System/ or /usr/libexec/, so a user process cannot spoof healthy entries) in a single log show query. A ManagedClient Received HTTP response (200) entry is the MDM success evidence. APNs activity (apsd courier connections or incoming-message acknowledgements) without an MDM response reports APNs active; no MDM response as a warning, an MDM identity error -25304 newer than the last MDM response reports MDM identity error as a failure, and no APNs activity or MDM response at all is a failure. Each run logs the last APNs activity, last MDM response and courier connection-failure count.
Memory Pressure records one sample whenever its check executes, including full health-check runs, nightly Silent Client-Side Cache refreshes, and targeted rechecks of an existing memory-pressure warning. It warns only when yellow or red pressure was observed on at least two distinct local calendar days within the previous seven days. A single critical reading, repeated runs on one day, low free-memory percentage, and swap use alone do not trigger a warning. Fewer than two days with valid pressure levels, an unavailable current pressure level, or a history write failure produce Insufficient data without changing the run's exit code. Cached Splunk uploads, healthy Inspect replay, and synthetic Test runs do not collect a sample.
History defaults to ${organizationDirectory}/MacHealthCheck-MemoryPressure-History.jsonl, owned by root with 600 permissions. Each JSON Lines record contains ISO8601 and epoch timestamps, local sample date, hostname, script version, pressure level, free-memory percentage, and used swap in human-readable and byte forms. memoryPressureHistoryPath, memoryPressureHistoryRetentionDays (default 14), memoryPressureLookbackDays (default 7), and memoryPressureRequiredAdverseDays (default 2) are configurable in Mac-Health-Check.zsh. Invalid or unavailable readings stay unknown; history failures affect only this check. The JSON report and Inspect summary use stable check key memoryPressure.
App Auto-Patch prefers root-written logs: the 4.x system log (/Library/Application Support/AppAutoPatch/logs/aap.log), then the 3.x log (/Library/Management/AppAutoPatch/logs/aap.log). It reads the user-writable per-user ~/Library/Logs/AppAutoPatch/aap.log only when neither exists (logged as a user-reported log), and ignores 4.x Discovery complete timestamps more than 5 minutes in the future.
Beginning in 5.0.1, the VPN Client check (with vpnClientVendor="paloalto", the default) and the sample Palo Alto Networks GlobalProtect Status.bash external check (0.0.5) no longer trust only the DEM keys in com.paloaltonetworks.GlobalProtect.settings.plist, which can stay disconnected while connected (for example, when dem-agent is not installed). A live utun* / gpd* tunnel IPv4 that matches the DEM tunnel-ip or a PanGPS PreferredIP_<hash> value reports Connected <ip>; otherwise, a single tunnel IPv4 while PanGPS is running reports Connected <ip> (multiple tunnels are never guessed); otherwise, the DEM tunnel-status decides, with unchanged status strings.
Jamf Pro inventory submission is a final follow-up action. In full Jamf Pro runs, updateComputerInventory() now surfaces failed or timed-out jamf recon submissions to the end-user, and times out that submission after 90 seconds.
- Generates a structured JSON health report at the end of every run
- Saves the report locally to
/Library/Management/org.churchofjesuschrist/MacHealthCheck-Report.jsonby default with600permissions - Keeps
/Library/Management/org.churchofjesuschrist/MacHealthCheck-Report.jsonas the canonical root-only report artifact - Adds
identity.entraIDRegistrationwithstatus,method,lastUser,lastUserHome, anddetails - Supports optional Splunk HEC delivery through Parameters 6-10 (see Script Parameters) without changing the existing
operationModecontract - Wraps the finalized report as
{sourcetype, index, event}when posting to Splunk HEC - Supports
splunkOperationMode=offto disable HEC delivery explicitly while still preserving local JSON report generation - Preserves local report generation in
splunkOperationMode=testwhile intentionally skipping network transmission - Treats any unrecognized
splunkOperationModevalue astest(logged as[ERROR]); only an explicitproductionenables HEC delivery SilentplussplunkOperationMode=productionmirrorsSplunk Reporting:lines to stdout and keeps all other logged run output in${scriptLog}(a few early pre-logging messages, such as MDM-detection notices, can still print), and final exit returns success when local report generation plus HEC delivery both succeed, regardless of recorded health findings- That
SilentplussplunkOperationMode=productionpath also skips final Jamf Pro inventory submission while logging the skip to${scriptLog} - Client-Side Cache avoids a full
Silent+splunkOperationMode=productionhealth-check run when the client-side script at/Library/Management/org.churchofjesuschrist/MHC.zshmatches the server-side version and the cached JSON report is valid and fresh - The client-side nightly run defaults to
operationMode="Silent"andsplunkOperationMode="test"so it updates the local report without sending to production Splunk, and its LaunchDaemon routes stdout/stderr to/dev/nullso MHC-prefixed log writes are not duplicated - Requires
jqfor JSON validation and formatting, with local report generation and Splunk payload assembly stopping at pre-flight ifjqis unavailable;/usr/bin/jq(macOS 15 and later) is preferred, and/usr/local/bin/jqor/opt/homebrew/bin/jqis used only when the binary and every parent directory are root-owned and not group- or world-writable (a user-owned Homebrewjqis rejected) TestandDevelopmentruns write their local report to/Library/Management/org.churchofjesuschrist/MacHealthCheck-Report-<mode>.jsoninstead of the canonical report, skip Splunk HEC delivery, and do not install the Client-Side Cache copy, so synthetic or curated results never replace production data; cached uploads andSelf Servicetargeted rechecks also reject canonical reports whosemetadata.operationModeis notSelf ServiceorSilent
Self Serviceand fullSilenthealth-check runs now generate/Library/Application Support/org.churchofjesuschrist/Inspect/MacHealthCheck-Inspect-Config.jsondirectly from finalized in-memory resultsSelf Serviceand fullSilenthealth-check runs also generate/Library/Application Support/org.churchofjesuschrist/Inspect/MacHealthCheck-Inspect-Compliance.plist, which feedsplistSources,compliance-summary,findings-listand live-bound bento-grid popovers- The generated config includes
/Library/Application Support/org.churchofjesuschrist/Inspect/Users/<user>/MacHealthCheck-Inspect.trigger,/Library/Application Support/org.churchofjesuschrist/Inspect/Users/<user>/MacHealthCheck-Inspect.readyand/Library/Application Support/org.churchofjesuschrist/Inspect/Users/<user>/MacHealthCheck-Inspect-Result.jsoncontrol paths for Inspect Mode workflows Self Serviceautomatically rechecks non-healthy.checks[].keyvalues when the canonical report has a matching device, MDM vendor, script version and full-run baseline less than 36 hours old- Targeted dialogs display contiguous check numbers immediately, while Inspect presents the authoritative Mac Health Check status separately from swiftDialog's weighted
Compliance Score - Jamf Pro targeted rechecks also run
Computer Inventoryso verified remediation reaches the server; nightly Client-Side Cache reports (which omitComputer Inventory) remain valid targeted-recheck and replay baselines - Targeted rechecks send Slack or Microsoft Teams webhook messages (Jamf Pro) only when at least one rechecked status differs from the previous report
- Targeted results replace only matching check records, preserve untouched results, add
checkedAt/checkedAtEpoch, and exposemetadata.runScope, full-run baseline fields andsummary.recheckedCount - Targeted writes use a shared report lock and rebase onto compatible concurrent report updates so newer full-report data is not overwritten
- Targeted writes cannot extend the 36-hour age of their underlying full-run baseline; missing, stale, malformed or incompatible reports fall back to all checks
- Parameter 11
forceFreshRun=trueand/var/tmp/MacHealthCheck-Force-Fresh-Runbypass targeted verification and cached replay for a fullSelf Servicerun; the trigger file is honored only when root-owned (for example,sudo touch /var/tmp/MacHealthCheck-Force-Fresh-Run), and a trigger created by any other user is ignored and removed Silent,Debug,DevelopmentandTestretain their existing execution behavior; targeted selection is confined toSelf Service- Unhealthy runs now surface
Quick Actions RecommendedinOverview, add a conditionalRemediation Guidestep immediately afterOverview, and keepUnhealthyas the audit-detail step - Category bento-grid cards now use status-aware backgrounds so unhealthy checks stand out more clearly in Preset 6, and
Available Updatesnow expands across two columns when action is required - Plist-backed bento cells now emit FR #667 detail-sheet fields (
severity,explanation,remediation,actionButtonText,actionURL) so warning and failure cards can explain why action is needed and link directly to next steps - Normal
Self Serviceruns launch the detached, always-on-top, moveable, minimisable swiftDialog Inspect Modepreset6guided summary after report generation while retaining the existing 60-second main-dialog countdown Silentruns never launch swiftDialog; they only write Inspect Mode config assets for later review- The detached summary now separates recorded results into
UnhealthyandHealthysections and omits either section when no checks exist in that bucket - Re-running
sudo zsh --no-rcs Mac-Health-Check.zshinSelf Servicewith a healthy report withininspectReplayMaximumAgeSeconds(default900, 15 minutes) replays the cached inspect summary immediately and skips the health checks plus the main dialog countdown - Unhealthy
Self Serviceruns now rely on the final unhealthy main-dialog state plus the detached inspect summary after report generation, without a separate pseudo-alert notification - If inspect-summary asset generation or launch fails, Mac Health Check falls back to the existing
completionTimercountdown path - Targets swiftDialog
3.1.1.4997or newer for PR #684 rendering; older compatible builds retain their prior Preset 6 appearance
Example Preset 6 JSON fragments used by generated inspect assets:
{
"key": "available_updates",
"displayName": "Available Updates",
"category": "Maintenance",
"isCritical": true,
"severity": "warning",
"explanation": "A macOS update is ready for this Mac. Installing current updates helps keep this Mac secure and aligned with Church standards.",
"remediation": "1. Open **System Settings**.\n2. Select **General > Software Update**.\n3. Install **macOS 26.5 (19-May-2026)**.\n4. Restart your Mac if prompted.\n5. Run **Mac Health Check** again.",
"actionButtonText": "Open Software Update",
"actionURL": "x-apple.systempreferences:com.apple.Software-Update-Settings.extension"
}{
"id": "support_resources",
"column": 0,
"row": 0,
"columnSpan": 3,
"title": "Help & Support",
"subtitle": "Action Recommended",
"sfSymbol": "person.crop.circle.badge.questionmark",
"contentType": "mixed",
"detailOverlay": {
"title": "Help & Support",
"subtitle": "Action Recommended",
"icon": "SF=person.crop.circle.badge.questionmark",
"content": [
{
"type": "info",
"content": "Use these support options if you need help completing recommended steps."
}
],
"showSystemInfo": false,
"showProgressInfo": false,
"closeButtonText": "Close"
}
}- Dynamic
supportLabel1/supportValue1throughsupportLabel6/supportValue6 - Empty Label / Value pairs are skipped automatically
- Legacy fallback still works when all dynamic pairs are empty:
- Telephone (
supportTeamPhone) - Email (
supportTeamEmail) - Website (
supportTeamWebsite) - Knowledge Base Article (
supportKBURL)
- Telephone (
- Info button target now uses the first URL-like dynamic support value; if none is found, it falls back to legacy Knowledge Base values
- Full Name
- User Name
- User ID
- Volume Owners
- Secure Token
- Location Services
- Microsoft OneDrive Sync Date
- Platform Single Sign-on Extension
- macOS version (build)
- System Memory
- System Storage
- Dialog version
- Script version
- Computer Name
- Serial Number
- Wi-Fi SSID
- IP Address (first active network service)
- VPN IP Address
- Jamf Pro Computer ID
- Site
** Requires Jamf Pro and a configuration profile that uses Payload Variables for Configuration Profiles
At the end of each run, Mac Health Check logs a one-line summary (wrapped below for readability), plus a second line on Jamf Pro:
MHC (5.0.1): 2026-10-14 08:15:42 - [NOTICE] WARNING: 'localadmin' IS A MEMBER OF 'admin';
User: macOS Server Administrator (localadmin) [503]; Security Mode: Full;
DEP-allowed MDM Control: Enabled; Activation Lock: Disabled; Bootstrap Token supported on server: YES;
Bootstrap Token escrowed to server: YES; Location Services: Enabled; SSH: On;
Microsoft OneDrive Sync Date: Not Configured; Time Machine Backup Date: Not configured;
Battery Cycle Count: 12; Rosetta-required apps: None detected; Wi-Fi: Liahona; VPN: None;
Network Time Server: time.apple.com
MHC (5.0.1): 2026-10-14 08:15:42 - [NOTICE] Jamf Pro Computer ID: 007; Site: Servers
- Warning when logged-in user is a member of
admin - Logged-in user full name, username and UID
- Security Mode
- DEP-allowed MDM Control
- Activation Lock
- Bootstrap Token
- Location Services
- SSH
- Microsoft OneDrive Sync Date
- Time Machine destinations and last backup date
- Battery Cycle Count
- Rosetta-required apps
- Wi-Fi SSID
- VPN status
- Network Time Server
- Jamf Pro Computer ID and Site (Jamf Pro only)
Community-supplied, best-effort support is available on the Mac Admins Slack (free, registration required) #mac-health-check Channel, or you can open an issue.

Deployment of Mac Health Check involves configuring organizational defaults, uploading the script to your MDM server, creating a policy to run it on demand and testing to ensure proper output and behavior.
Skills/mac-health-check-selector is an AI-agnostic skill for choosing which checks to enable. It asks which MDM you use, presents a categorized checklist, and, after you confirm, writes an edited, MDM-specific, date-stamped copy, Mac-Health-Check_<mdm-slug>_<YYYY-MM-DD-HHMMSS>.zsh, with a sidecar .md that records the selection. Both files go to Artifacts/ next to the source Mac-Health-Check.zsh by default; the helper's --out-dir <dir> overrides that, but the target must still be git-ignored (validation check 8). Only the MDM's list-item array and its matching runConfiguredHealthCheck branch change, unless you also accept the optional prompt to remove other MDMs' code: that removes every other MDM's list-item array, its branches in each vendor case block (including serverURL detection), and vendor-only functions your selection doesn't call, while keeping the generic fallback so Macs enrolled elsewhere still run the MDM-agnostic checks. Otherwise, operationMode, developmentListitemJSON, and every other setting keep their Mac-Health-Check.zsh defaults, so edit the artifact manually to change them. The skill builds and validates each artifact with its tested helper, Skills/mac-health-check-selector/scripts/build-artifact.zsh. The helper prints an explicit PASS or FAIL for zsh -n, jq, row-to-call alignment (including M15 and F1 order), diff scope, a Client-Side Cache simulation, scriptVersion, an unchanged source, and git-ignored output. It exits non-zero on any failure, and a failed build never lands in the output folder. Before deploying, test the artifact in all five modes on one Mac enrolled in the chosen MDM. Mac-Health-Check.zsh itself is never modified, and Artifacts/ is git-ignored (see Artifacts/README.md).
- Get the repository. Clone
dan-snelson/Mac-Health-Checkor download a5.0.0+ release. The skill readsMac-Health-Check.zshfrom the same folder tree. - Open the repository root in an AI assistant that can read and write local files and run shell commands (for example, Claude Code, Codex CLI, Cursor, or GitHub Copilot agent mode). Without file access, the skill prints the artifact and sidecar as code blocks for you to save under
Artifacts/yourself. - Load the skill. Prompt:
Read Skills/mac-health-check-selector/SKILL.md and follow it to help me choose Mac Health Check checks.Assistants that honorAGENTS.mdpick it up automatically.- Claude Code (optional):
mkdir -pv .claude/skills && ln -s ../../Skills/mac-health-check-selector .claude/skills/mac-health-check-selector, then run/mac-health-check-selector.
- Claude Code (optional):
- Answer the MDM question (
1–9; Filewave is8, Other / MDM-agnostic is9). Kandji / Iru admins: confirm your server URL, because only URLs containingkandjiare detected. - Pick checks from the categorized checklist with IDs, ranges, or categories. Every reply changes the current list:
H7 A4adds,no A3removes,only C, H1-H6, M4picks exactly those, anddefaultsresets. Unmarked checks are on by default,[off]checks are available but off, and*marks external checks (Jamf Pro only). A4 uses Microsoft Teams unless you name another app. - Review the confirmation summary (MDM, enabled check IDs, changes against the shipped default, artifact name), then reply
yes, or adjust with the same grammar. Nothing is written before you confirm. - Choose whether to remove other MDMs' code (
yesorno; defaultno).yessuits a fleet on a single MDM, such as Jamf Pro only; keep the default for a mixed fleet. - Check the results. The assistant reports the artifact path, the sidecar
.mdpath, PASS/FAIL for validation checks 1–8 (plus 4b and 4c when other MDMs' code was removed), and the report keys the change removes or adds (for example,electron_corner_mask). The helper writes the complete sidecar, including every disabled check with its reason and the dependency notes that apply. Only passing artifacts are written toArtifacts/(the default output folder). - Adjust other settings manually (for example,
operationModeor external-check triggers) in the artifact if needed, then re-runzsh -non it. - Test on one Mac enrolled in the chosen MDM in all five modes:
sudo zsh --no-rcs ./Artifacts/<file>.zsh "" "" "" "Self Service", then repeat withSilent,Debug,Development, andTestas Parameter 4.Developmentruns the shippeddevelopmentListitemJSONsubset, not your selection. The script picks its MDM branch from the enrolled server URL (see MDM Detection), so a Mac enrolled in a different MDM runs that MDM's unedited checks, or the MDM-agnostic fallback when you removed other MDMs' code; test ageneric(Other / MDM-agnostic) artifact on an unenrolled Mac or one whose MDM is not detected.TestandDevelopmentruns never touch the canonical report or the Client-Side Cache copy.Self ServiceandDebugruns replace the Client-Side Cache copy and LaunchDaemon only when the running script is a root-owned file in root-controlled directories, so a copy run from a user-owned checkout logsinstall skipped; re-run your production policy afterwards. - Deploy the artifact as your MDM script. Keep
DebugandDevelopmentout of production policies, and expect the first Self Service run to be a full run (check_set_mismatch). - Keep artifacts out of version control (
Artifacts/is git-ignored), and build a fresh artifact after each Mac Health Check upgrade or for each additional MDM.
The "Development" Operation Mode (introduced in 3.0.0) aids in developing Health Checks, allowing quick runs against a small curated subset instead of the full suite.
When operationMode is set to Development, 5.0.1 uses a dedicated developmentListitemJSON for Clock Skew, Memory Pressure and Apple Push Notification service instead of running the entire suite.
####################################################################################################
#
# Program
#
####################################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Generate dialogJSONFile based on Operation Mode and MDM Vendor
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if [[ "${operationMode}" == "Development" ]]; then
notice "Operation Mode is ${operationMode}; using ${operationMode} dialogJSONFile template."
# Development List Items
developmentListitemJSON='
[
{"title" : "Clock Skew", "subtitle" : "Checks local clock offset against time.apple.com", "icon" : "SF=01.circle,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …", "iconalpha" : 0.5},
{"title" : "Memory Pressure", "subtitle" : "Reviews memory pressure across recent days", "icon" : "SF=02.circle,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …", "iconalpha" : 0.5},
{"title" : "Apple Push Notification service", "subtitle" : "Validate communication between Apple, '${mdmVendor}' and your Mac", "icon" : "SF=03.circle,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …", "iconalpha" : 0.5}
]
'
# Validate developmentListitemJSON is valid JSON
if ! validateJson "${developmentListitemJSON}"; then
echo "Error: developmentListitemJSON is invalid JSON"
echo "$developmentListitemJSON"
exit 1
else
combinedJSON=$( mergeDialogAndListItems "${mainDialogJSON}" "${developmentListitemJSON}" )
fi
elseAdditionally, the matching Health Check functions are executed:
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Generate Health Checks based on Operation Mode and MDM Vendor (where "n" represents the listitem order)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if [[ "${operationMode}" == "Development" ]]; then
# Operation Mode: Development
notice "Operation Mode is ${operationMode}; using ${operationMode}-specific Health Check."
dialogUpdate "title: ${humanReadableScriptName} (${scriptVersion})<br>Operation Mode: ${operationMode}"
# set -x
checkClockSkew "0"
checkMemoryPressure "1"
checkAPNs "2"
# set +x
else












