Menu Close

DDM OS Reminder (2.1.0)

A maintenance release to Mac Admins’ new favorite, MDM-agnostic, “set-it-and-forget-it” end-user reminder for Apple’s Declarative Device Management-enforced macOS update deadlines that further simplifies enterprise-wide deployment and adds user warnings for excessive uptime and low disk space

Overview

While Apple’s Declarative Device Management (DDM) provides Mac Admins a powerful way to enforce macOS updates, its built-in notification is often too subtle for most administrators:

Apple’s built-in Notification is often too subtle for most Mac Admins

DDM OS Reminder evaluates the most recent EnforcedInstallDate and setPastDuePaddedEnforcementDate entries in /var/log/install.log, and then leverages a swiftDialog-enabled script plus a LaunchDaemon to deliver a more prominent end-user dialog that reminds users to update their Mac to comply with DDM-enforced macOS update deadlines.

Features

Mac Admins can configure daysBeforeDeadlineBlurscreen to control how many days before the DDM-specified deadline the screen blurs when displaying your customized reminder dialog

Mac Admins can configure daysBeforeDeadlineBlurscreen to control how many days before the DDM-specified deadline the screen blurs when displaying your customized reminder dialog

  • Customizable: Easily customize the reminder dialog’s title, message, icons and button text to fit your organization’s requirements by distributing a Configuration Profile via any MDM solution.
  • Easy Installation: The assemble.zsh script makes it easy to deploy your reminder dialog and display frequency customizations via any MDM solution, enabling quick rollout of DDM OS Reminder organization-wide.
  • Set-it-and-forget-it: Once configured and installed, a LaunchDaemon displays your customized reminder dialog — automatically checking the installed macOS version against the DDM-required version — to remind users if an update is required.
  • Deadline Awareness: Whenever a DDM-enforced macOS version or its deadline is updated via your MDM solution, the reminder dialog dynamically updates the countdown to both the deadline and required macOS version to drive timely compliance.
  • Intelligently Intrusive: The reminder dialog is designed to be informative without being disruptive — it checks whether a user is in an online meeting before displaying — so users can remain productive while still being reminded to update.
  • Logging: The script logs its actions to your specified log file, allowing Mac Admins to monitor its activity and troubleshoot as necessary.
  • Demonstration Mode: A built-in demo mode allows Mac Admins to test the appearance and functionality of the reminder dialog with ease.

Implementation

1. Test Deployment

Jumpstart your DDM OS Reminder implementation by first conducting a test deployment on a non-production Mac which has swiftDialog installed. (Ideally, use a non-production Mac which is already in-scope of a pending Declarative Device Management-enforced macOS update from your MDM server.)

  1. Visit the DDM OS Reminder repository on GitHub
  2. Download the main branch by selecting Code > Download ZIP
Visit the DDM OS Reminder repository on GitHub.com and download the 'main' branch by selecting: Code > Download ZIP
  1. In an elevated Terminal session, change to the downloaded DDM-OS-Reminder-main directory
cd ~/Downloads/DDM-OS-Reminder-main
  1. Execute the reminderDialog.zsh script in demo mode:
zsh reminderDialog.zsh demo
Execute the reminderDialog.zsh script in demo mode: zsh reminderDialog.zsh demo
  1. Review the reminder dialog and interact with each of its buttons, re-executing zsh reminderDialog.zsh demo as required
  2. Review the script’s output:
root@XDT8675309 DDM-OS-Reminder-main # zsh reminderDialog.zsh demo
dorm (2.1.0): 2025-12-11 12:00:31 - [PRE-FLIGHT]      

###
# DDM OS Reminder End-user Message (2.1.0)
# http://snelson.us/ddm
####

dorm (2.1.0): 2025-12-11 12:00:31 - [PRE-FLIGHT]      Initiating …
dorm (2.1.0): 2025-12-11 12:00:31 - [PRE-FLIGHT]      Check for Logged-in System Accounts …
dorm (2.1.0): 2025-12-11 12:00:31 - [PRE-FLIGHT]      Current Logged-in User: dan
dorm (2.1.0): 2025-12-11 12:00:31 - [PRE-FLIGHT]      Current Logged-in User First Name (ID): Dan (502)
dorm (2.1.0): 2025-12-11 12:00:31 - [PRE-FLIGHT]      Complete
dorm (2.1.0): 2025-12-11 12:00:31 - [NOTICE]          Demo mode enabled
dorm (2.1.0): 2025-12-11 12:00:34 - [NOTICE]          Display Reminder Dialog to dan with additional options: --ontop
dorm (2.1.0): 2025-12-11 12:00:53 - [INFO]            Return Code: 0
dorm (2.1.0): 2025-12-11 12:00:53 - [NOTICE]          dan clicked Open Software Update
dorm (2.1.0): 2025-12-11 12:00:54 - [NOTICE]          Checking if System Settings is open …
dorm (2.1.0): 2025-12-11 12:00:54 - [INFO]            System Settings is open; Telling System Settings to make a guest appearance …
dorm (2.1.0): 2025-12-11 12:00:54 - [QUIT]            Exiting …
dorm (2.1.0): 2025-12-11 12:00:54 - [QUIT]            Keep them movin' blades sharp!
  1. Simulate the installation of a .plist client-side by manually copying sample.plist to one of its expected locations with its expected filename:
cp -v Resources/sample.plist /Library/Preferences/org.churchofjesuschrist.dorm.plist
  1.  Re-execute zsh reminderDialog.zsh demo and confirm you now observe the word “Sample” in various places in the reminder dialog, as configured in sample.plist:
 Re-execute zsh reminderDialog.zsh demo and confirm you now observe the word "Sample" in various places in the reminder dialog, as configured in sample.plist
2. Basic Deployment

A basic deployment of DDM OS Reminder starts by specifying your organization’s Reverse Domain Name Notation value when prompted by the assemble.zsh script

  1. Generate customized deployment artifacts for your organization by using the assemble.zsh script, specifying your organization’s Reverse Domain Name Notation (i.e., com.company) when prompted:
zsh assemble.zsh
===============================================================
🧩 Assemble DDM OS Reminder (2.1.0)
===============================================================

Full Paths:

        Reminder Dialog: /Users/dan/Downloads/DDM-OS-Reminder/reminderDialog.zsh
LaunchDaemon Management: /Users/dan/Downloads/DDM-OS-Reminder/launchDaemonManagement.zsh
      Working Directory: /Users/dan/Downloads/DDM-OS-Reminder
    Resources Directory: /Users/dan/Downloads/DDM-OS-Reminder/Resources

🔍 Checking Reverse Domain Name Notation …

    Reminder Dialog (reminderDialog.zsh):
        reverseDomainNameNotation = org.churchofjesuschrist
        organizationScriptName    = dorm

    LaunchDaemon Management (launchDaemonManagement.zsh):
        reverseDomainNameNotation = org.churchofjesuschrist
        organizationScriptName    = dor


Enter Your Organization’s Reverse Domain Name Notation [org.churchofjesuschrist] (or 'X' to exit): us.snelson

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Using 'us.snelson' as the Reverse Domain Name Notation
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

🔧 Inserting reminderDialog.zsh into launchDaemonManagement.zsh  …

✅ Assembly complete [2025-12-12-035844]
   → Resources/ddm-os-reminder-assembled-2025-12-12-035844.zsh

🔁 Updating reverseDomainNameNotation to 'us.snelson' in assembled script …

🔍 Performing syntax check on 'Resources/ddm-os-reminder-assembled-2025-12-12-035844.zsh' …
    ✅ Syntax check passed.

🗂  Generating LaunchDaemon plist …
    🗂  Creating us.snelson.dorm plist from Resources/sample.plist …

    🔧 Updating internal plist content …
   → Resources/us.snelson.dorm-2025-12-12-035844.plist

🧩 Generating Configuration Profile (.mobileconfig) …
   → Resources/us.snelson.dorm-2025-12-12-035844-unsigned.mobileconfig

🔍 Performing syntax check on 'Resources/us.snelson.dorm-2025-12-12-035844-unsigned.mobileconfig' …
    ✅ Profile syntax check passed.

🔁 Renaming assembled script …

🔁 Updating scriptLog path based on RDNN …

🏁 Done.

Deployment Artifacts:
        Assembled Script: Resources/ddm-os-reminder-us.snelson-2025-12-12-035844.zsh
    Organizational Plist: Resources/us.snelson.dorm-2025-12-12-035844.plist
   Configuration Profile: Resources/us.snelson.dorm-2025-12-12-035844-unsigned.mobileconfig

===============================================================
  1. Carefully review each deployment artifact and distribute the appropriate files to a single test Mac via your MDM.
  1. Monitor the client-side log file, using the following as an example (substitute your organization’s Reverse Domain Name Notation and use Control-C to break):
tail -f /var/log/org.churchofjesuschrist.log 
  1. Kickstart the DDM OS Reminder LaunchDaemon, using the following as an example (substitute your organization’s Reverse Domain Name Notation):
launchctl kickstart -kp system/org.churchofjesuschrist.dor
  1. In your preferred code editor — being careful to maintain the XML-escaped text — modify the various string values in the your customized .plist or .mobileconfig to suit your organization:
    • Logging
      • ScriptLog
    • Reminder Timing
      • DaysBeforeDeadlineDisplayReminder
      • DaysBeforeDeadlineBlurscreen
      • DaysBeforeDeadlineHidingButton2
      • DaysOfExcessiveUptimeWarning
      • MinimumDiskFreePercentage
      • MeetingDelay
    • Branding
      • OrganizationOverlayIconURL
      • SwapOverlayAndLogo
    • Support
      • SupportTeamName
      • SupportTeamPhone
      • SupportTeamEmail
      • SupportTeamWebsite
      • SupportKB
      • InfoButtonAction
      • SupportKBURL
    • Dialog
      • Title
      • Button1Text
      • Button2Text (set to <string></string> to suppress displaying
      • InfoButtonText (set to hide to supress displaying)
      • ExcessiveUptimeWarningMessage
      • DiskSpaceWarningMessage
      • Message
    • Infobox
      • InfoBox
    • Help
      • HelpMessage
      • HelpImage (set to hide to supress displaying)
  2. Distribute the updated .plist or .mobileconfig to your test Mac
  3. Kickstart the DDM OS Reminder LaunchDaemon, using the following as an example (substitute your organization’s Reverse Domain Name Notation):
launchctl kickstart -kp system/org.churchofjesuschrist.dor
  1. Monitor the client-side log file, using the following as an example (substitute your organization’s Reverse Domain Name Notation and use Control-C to break):
tail -f /var/log/org.churchofjesuschrist.log 
3. Advanced Deployment

An advanced deployment of DDM OS Reminder leverages a customized LaunchDaemon for fine-grained control of when the reminder dialog is displayed to your users

  1. In your preferred code editor, modify the StartCalendarInterval in launchDaemonManagement.zsh to match your organization’s requirements; launchd.info is a great reference
cat <<ENDOFLAUNCHDAEMON
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>${launchDaemonLabel}</string>
    <key>UserName</key>
    <string>root</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/zsh</string>
        <string>${organizationDirectory}/${organizationScriptName}.zsh</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/usr/bin:/bin:/usr/sbin:/sbin:/usr/local:/usr/local/bin</string>
    </dict>
    <key>StartCalendarInterval</key>
    <array>
        <dict>
            <key>Hour</key>
            <integer>8</integer>
            <key>Minute</key>
            <integer>0</integer>
        </dict>
        <dict>
            <key>Hour</key>
            <integer>16</integer>
            <key>Minute</key>
            <integer>0</integer>
        </dict>
    </array>
    <key>StandardErrorPath</key>
    <string>${scriptLog}</string>
    <key>StandardOutPath</key>
    <string>${scriptLog}</string>
</dict>
</plist>

ENDOFLAUNCHDAEMON
  1. Next, use your preferred code editor to modify the hard-coded random delay in reminderDialog.zsh to match the modifications you made in Step 1.
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# If Update Required, Display Dialog Window (respecting Display Reminder threshold)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

if [[ "${versionComparisonResult}" == "Update Required" ]]; then

    # Skip notifications if we're outside the display reminder window (thanks for the suggestion, @kristian!)
    if (( ddmVersionStringDaysRemaining > daysBeforeDeadlineDisplayReminder )); then
        notice "Deadline still ${ddmVersionStringDaysRemaining} days away; skipping reminder until within ${daysBeforeDeadlineDisplayReminder}-day window."
        quitScript "0"
    else
        notice "Within ${daysBeforeDeadlineDisplayReminder}-day reminder window; proceeding with reminder."
    fi

    # Confirm the currently logged-in user is "available" to be reminded
    # If the deadline is more than 24 hours away, and the user has an active Display Assertion, exit the script
    if [[ "${ddmVersionStringDaysRemaining}" -gt 1 ]]; then
        if checkUserDisplaySleepAssertions; then
            notice "No active Display Sleep Assertions detected; proceeding with reminder."
        else
            notice "Presentation still active after ${meetingDelay} minutes; exiting quietly."
            quitScript "0"
        fi
    else
        info "Deadline is within 24 hours; ignoring ${loggedInUser}'s Display Sleep Assertions."
    fi

    # Randomly pause script during its launch hours of 8 a.m. and 4 p.m.; Login pause of 30 to 90 seconds
    currentHour=$(( $(date +%H) ))
    currentMinute=$(( $(date +%M) ))

    if (( currentHour == 8 || currentHour == 16 )) && (( currentMinute == 0 )); then
        notice "Daily Trigger Pause: Random 0 to 20 minutes"
        sleepSeconds=$(( RANDOM % 1200 ))
    else
        notice "Login Trigger Pause: Random 30 to 90 seconds"
        sleepSeconds=$(( 30 + RANDOM % 61 ))
    fi

    if (( sleepSeconds >= 60 )); then
        (( pauseMinutes = sleepSeconds / 60 ))
        (( pauseSeconds = sleepSeconds % 60 ))
        if (( pauseSeconds == 0 )); then
            humanReadablePause="${pauseMinutes} minute(s)"
        else
            humanReadablePause="${pauseMinutes} minute(s), ${pauseSeconds} second(s)"
        fi
    else
        humanReadablePause="${sleepSeconds} second(s)"
    fi
    info "Pausing for ${humanReadablePause} …"
    sleep "${sleepSeconds}"

    # Update Required Variables
    updateRequiredVariables

    # Display reminder dialog (with blurscreen, depending on proximity to deadline)
    displayReminderDialog --ontop

else

    info "Version Comparison Result: ${versionComparisonResult}"

fi



# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Exit
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

exit 0
  1. Execute the assemble.zsh script to insert your customized reminderDialog.zsh script into your customized launchDaemonManagement.zsh script and to create the deployment artifacts
  2. Carefully review each deployment artifact and distribute the appropriate files to a single test Mac via your MDM
  3. Kickstart the DDM OS Reminder LaunchDaemon
  4. Monitor the client-side log file
4. Upgrading

Author’s Highly Opinionated Thought: There are enough changes in version 2.x that it’s probably easier to just start-from-scratch — optionally uninstalling earlier versions — rather than attempting to update from a previous version of DDM OS Reminder.

Screencast 02:00 (no audio; edited for time)

The following is one method of using Visual Studio Code to upgrade your customized script to the latest version.

  1. Download the latest assembled version from the Resources directory on GitHub
    • You may also wish to review the various branches for pre-release versions
  2. Make note of scriptVersion in the freshly downloaded script
  3. Using a backup of your current, known-working version, open both files in VS Code
  4. Click the Explorer button (Command-Shift-E)
    • If necessary, enable “Open Editors” in Explorer’s options
  5. Individually right-click the files listed in the Open Editors to begin the comparison
    • Select for Compare: The freshly downloaded script
    • Compare with Selected: Your duplicated, renamed, customized script
  6. Close the Explorer
  7. Review each difference — shown in red — then click the right-facing arrow to replace the code in your duplicated, renamed, customized script with the code from the freshly downloaded script
    • Notes:
      • You can safely ignore any LaunchDaemon-related differences
      • Enabling View > Word Wrap can interfere with code comparison
  8. Test
  9. Deploy
5. Resources

Scripts

  1. Assemble combined, deployable artifacts of your customized scripts
  2. Create Self-extracting encodes the most recently assembled script for easier deployment with some MDMs
  3. Create .plist creates .plist and .mobileconfig, based on your customizations to both reminderDialog.zsh and launchDaemonManagement.zsh
  4. Extension Attributes were created for and tested on Jamf Pro and can most likely be adapted for other MDMs

Configuration Profile

Special thanks to Max Sundell for his write-up: Create and deploy a macOS configuration profile (.mobileconfig)

Testing Tips

The following have proved helpful during development and testing:

XTRACE

Execute the client-side reminder dialog script — under xtrace with a custom prompt — using the following as an example, substituting your organization’s Reverse Domain Name Notation:

  • Note: Once the reminder dialog appears, the last several output blocks tend to be the most informative
zsh -c 'PS4=" → "; zsh -x "$1"' -- /Library/Management/org.churchofjesuschrist/dor.zsh

Force-display

The following commands can be used to force-display the reminder dialog:

rdnn="org.churchofjesuschrist"
launchctl kickstart -kp "system/${rdnn}.dor"
tail -f /var/log/"${rdnn}".log

# Atomic Log Reset
lines=24
sed -i '' -e :a -e '$d;N;'"${lines}"',$D;ba' /var/log/"${rdnn}".log
launchctl kickstart -kp system/"${rdnn}".dor
tail -f /var/log/"${rdnn}".log

# Nuclear Log Reset
truncate -s 0 /var/log/"${rdnn}".log
launchctl kickstart -kp system/"${rdnn}".dor
tail -f /var/log/"${rdnn}".log

Support

Community-supplied, best-effort support is available on the Mac Admins Slack (free, registration required) #ddm-os-reminders channel, or you can open an issue.

Posted in Device Management, macOS, Scripts, swiftDialog, Tips & Tricks

Related Posts