Menu Close

DDM OS Reminder (3.1.0)

A major update to Mac Admins’ favorite MDM-agnostic, “set-it-and-forget-it” reminder now adds multiple language support, significantly more robust reminder display logic and streamlined upgrade functionality

Overview

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

🆕 DDM OS Reminder now resolves DDM-enforced macOS update deadlines from recent /var/log/install.log activity using a declaration-aware resolver that prioritizes applicable enforced-install signals over generic matches, suppressing reminders when declaration state is missing, conflicting, invalid, or no longer maps to an available update, and only honors setPastDuePaddedEnforcementDate when it safely matches the resolved declaration, before using a swiftDialog-enabled script and LaunchDaemon to deliver a more prominent end-user reminder dialog.

Features

  • 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. Before displaying, it checks for active display-sleep assertions from an allowlist of approved meeting apps, helping users stay 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.
  • Configurable Post-Deadline Restart Policy: Choose whether past-deadline devices are left alone, prompted to restart, or forced to restart (OffPromptForce) after your defined grace period, balancing user flexibility with reliable compliance.
  • 🆕 Upgrade-friendlyassemble.zsh can now import supported settings from a previously generated DDM OS Reminder .plist, infer the RDNN and deployment lane (dev, test, prod), and generate a matched assembled script, organizational .plist, and unsigned .mobileconfig in a single pass. (See 4. Upgrading.)
  • 🆕 Full Multi-language Experience: Version 3.0.0 fully supports English, German, French, Spanish, Portuguese, and Japanese across the reminder experience, with localized dialog content, support messaging, and human-readable deadline dates that automatically match the resolved language for a more polished, native-feeling user experience.

🆕 Localization Contributions

For additional language support, contributors only need to edit Resources/sample.plist. The runtime defaults and generated plist/mobileconfig output are derived from that localization surface.

See Language Translation: Italian for a real-world example.

For additional language support, contributors only need to edit Resources/sample.plist. The runtime defaults and generated plist/mobileconfig output are derived from that localization surface.

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
zsh reminderDialog.zsh demo prompt # Prompts user for restart confirmation
zsh reminderDialog.zsh demo prompt # Prompts user for restart confirmation
Are you sure you want to restart your computer now?
zsh reminderDialog.zsh demo force # Forces restart; use with caution
zsh reminderDialog.zsh demo force # Forces restart; use with caution
  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 ~ # cd /Users/dan/Downloads 
root@XDT8675309 Downloads # cd DDM-OS-Reminder-main 
root@XDT8675309 DDM-OS-Reminder-main # zsh reminderDialog.zsh demo
dorm (3.0.0): 2026-03-29 15:21:35 - [PRE-FLIGHT]      

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

dorm (3.0.0): 2026-03-29 15:21:35 - [PRE-FLIGHT]      Initiating …
dorm (3.0.0): 2026-03-29 15:21:35 - [PRE-FLIGHT]      Check for Logged-in System Accounts …
dorm (3.0.0): 2026-03-29 15:21:35 - [PRE-FLIGHT]      Current Logged-in User: dan
dorm (3.0.0): 2026-03-29 15:21:35 - [PRE-FLIGHT]      Current Logged-in User First Name (ID): Dan (502)
dorm (3.0.0): 2026-03-29 15:21:35 - [PRE-FLIGHT]      No client-side preferences found; using script-defined defaults
dorm (3.0.0): 2026-03-29 15:21:35 - [PRE-FLIGHT]      Complete
dorm (3.0.0): 2026-03-29 15:21:35 - [NOTICE]          Demo mode enabled
dorm (3.0.0): 2026-03-29 15:21:35 - [NOTICE]          Check dan’s Display Sleep Assertions
dorm (3.0.0): 2026-03-29 15:21:35 - [INFO]            Acceptable assertion application names (allowlist): MSTeams zoom.us Webex
dorm (3.0.0): 2026-03-29 15:21:35 - [INFO]            dan’s Display Sleep Assertion has ended after 0 minute(s).
dorm (3.0.0): 2026-03-29 15:21:35 - [NOTICE]          No active Display Sleep Assertions detected; proceeding …
dorm (3.0.0): 2026-03-29 15:21:35 - [NOTICE]          Light mode detected; using standard overlay icon
dorm (3.0.0): 2026-03-29 15:21:35 - [NOTICE]          Processing overlay icon from 'https://use2.ics.services.jamfcloud.com/icon/hash_2d64ce7f0042ad68234a2515211adb067ad6714703dd8ebd6f33c1ab30354b1d'
dorm (3.0.0): 2026-03-29 15:21:35 - [INFO]            Overlay icon appears to be a remote URL; downloading with curl
dorm (3.0.0): 2026-03-29 15:21:35 - [INFO]            Successfully downloaded overlay icon
dorm (3.0.0): 2026-03-29 15:21:36 - [INFO]            swiftDialog 3.0.0.4934 supports markdown color; rendering enforcement sentence in red.
dorm (3.0.0): 2026-03-29 15:21:36 - [NOTICE]          Display Reminder Dialog to dan with additional options: --ontop
dorm (3.0.0): 2026-03-29 15:21:42 - [INFO]            Return Code: 0
dorm (3.0.0): 2026-03-29 15:21:42 - [NOTICE]          dan clicked Open Software Update
dorm (3.0.0): 2026-03-29 15:21:43 - [NOTICE]          Checking if System Settings is open …
dorm (3.0.0): 2026-03-29 15:21:43 - [INFO]            System Settings is open; Telling System Settings to make a guest appearance …
dorm (3.0.0): 2026-03-29 15:21:43 - [QUIT]            Exiting …
dorm (3.0.0): 2026-03-29 15:21:43 - [QUIT]            When the sun beats down and I lie on the bench …
  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:
2. Basic Deployment

A basic deployment of DDM OS Reminder starts by specifying your organization’s Reverse Domain Name Notation value with 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) with interactive mode:
zsh assemble.zsh us.snelson --interactive
zsh assemble.zsh us.snelson --interactive

===============================================================
🧩 Assemble DDM OS Reminder (3.0.0)
===============================================================

Full Paths:

        Reminder Dialog: ~/Downloads/DDM-OS-Reminder/reminderDialog.zsh
LaunchDaemon Management: ~/Downloads/DDM-OS-Reminder/launchDaemonManagement.zsh
      Working Directory: ~/Downloads/DDM-OS-Reminder
    Resources Directory: ~/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


📥 RDNN provided via command-line argument: 'us.snelson'

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Interactive Configuration
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Drag-and-drop an earlier DOR .plist to import [Return to skip] (or ‘X’ to exit): 

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

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
IT Support, Branding & Restart Policy (Interactive)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Support Team Name [IT Support] (or ‘X’ to exit): Support
Support Team Phone [+1 (801) 555-1212] (or ‘X’ to exit): +1 (937) 555-1212
Support Team Email [rescue@snelson.us] (or ‘X’ to exit): support@snelson.us
Support Team Website [https://support.snelson.us] (or ‘X’ to exit): 
Knowledge Base ('YES' to specify; 'NO' to hide) [YES] (or ‘X’ to exit): YES
Support KB Title [Update macOS on Mac] (or ‘X’ to exit): KB8675309
Info Button Action [https://support.snelson.us/KB8675309] (or ‘X’ to exit): 
Support KB Markdown Link [[KB8675309](https://support.snelson.us/KB8675309)] (or ‘X’ to exit): 
Overlay Icon URL (Light) [https://use2.ics.services.jamfcloud.com/icon/hash_2d64ce7f0042ad68234a2515211adb067ad6714703dd8ebd6f33c1ab30354b1d] (or ‘X’ to exit): 
Overlay Icon URL (Dark) [https://use2.ics.services.jamfcloud.com/icon/hash_d3a3bc5e06d2db5f9697f9b4fa095bfecb2dc0d22c71aadea525eb38ff981d39] (or ‘X’ to exit): 
Swap Overlay and Logo (YES/NO) [NO] (or ‘X’ to exit): NO
Past-deadline Restart Behavior (Off / [P]rompt / [F]orce) [Off] (or ‘X’ to exit): P
Days Past Deadline Before Restart Workflow (0-999) [2] (or ‘X’ to exit): 

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Select Deployment Mode:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  1) Development - Keep placeholder text for local testing
  2) Testing     - Replace placeholder text with 'TEST' for staging
  3) Production  - Remove placeholder text for clean deployment

  [Press ‘X’ to exit]

Enter mode [1/2/3]: 2

📦 Deployment Mode: test

🔧 Inserting reminderDialog.zsh into launchDaemonManagement.zsh  …

✅ Assembly complete [2026-03-29-163015]
   → Artifacts/ddm-os-reminder-assembled-2026-03-29-163015.zsh

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

🔍 Performing syntax check on 'Artifacts/ddm-os-reminder-assembled-2026-03-29-163015.zsh' …
    ✅ Syntax check passed.

🗂  Generating LaunchDaemon plist …
    🗂  Creating us.snelson.dorm plist from ~/Downloads/DDM-OS-Reminder/Resources/sample.plist …

    🔧 Updating internal plist content …
    🧪 Testing mode: replacing placeholder text → 'TEST'
    🔧 Applying IT support, branding and restart policy values …
   → Artifacts/us.snelson.dorm-2026-03-29-163015-test.plist

🧩 Generating Configuration Profile (.mobileconfig) …
   → Artifacts/us.snelson.dorm-2026-03-29-163015-test-unsigned.mobileconfig

🔍 Performing syntax check on 'Artifacts/us.snelson.dorm-2026-03-29-163015-test-unsigned.mobileconfig' …
    ✅ Profile syntax check passed.

🔁 Renaming assembled script …

🔁 Updating scriptLog path based on RDNN …

🏁 Done.

Deployment Artifacts:
        Assembled Script: Artifacts/ddm-os-reminder-us.snelson-2026-03-29-163015-test.zsh
    Organizational Plist: Artifacts/us.snelson.dorm-2026-03-29-163015-test.plist
   Configuration Profile: Artifacts/us.snelson.dorm-2026-03-29-163015-test-unsigned.mobileconfig

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚠️  Important Next Steps:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  Testing Artifacts Generated:
    - All placeholder text replaced with 'TEST'
    - Suitable for staging/QA environments
    - NOT suitable for production use

===============================================================
  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 your customized .plist or .mobileconfig to suit your organization:
    • Logging
      • ScriptLog
    • Reminder Timing
      • DaysBeforeDeadlineDisplayReminder
      • DaysBeforeDeadlineBlurscreen
      • DaysBeforeDeadlineHidingButton2
      • DaysOfExcessiveUptimeWarning
      • PastDeadlineRestartBehavior
      • DaysPastDeadlineRestartWorkflow
      • MinimumDiskFreePercentage
      • MeetingDelay
      • AcceptableAssertionApplicationNames
    • Branding
      • OrganizationOverlayIconURL
      • OrganizationOverlayIconURLdark
      • 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

The default StartCalendarInterval is tuned for our environment: We’re expected to be at our desks between 8 a.m. to 5 p.m., with meetings starting at 9 a.m. and concluding by 4 p.m. So, users will be prompted once during the start of their work day and again at the close of their workday. (RunAtLoad is also set to true, so a restart may also trigger a reminder to be displayed.)

  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

    # -------------------------------------------------------------------------
    # Deadline window and periodic reminder logic (thanks for the suggestion, @kristian!)
    # -------------------------------------------------------------------------

    quietPeriodSeconds=4560     # 76 minutes (60 minutes + margin)
    periodicReminderDays=28     # 28 days
    periodicReminderSeconds=$(( periodicReminderDays * 86400 ))

    # Look for the most recent user interaction by Return Code
    # Return Code 0: User clicked Button 1 (Open Software Update)
    # Return Code 2: User clicked Button 2 (Remind Me Later)
    # Return Code 3: User clicked Info Button
    # Return Code 4: User allowed timer to expire
    # Return Code 10: User quit dialog with keyboard shortcut
    # These are the events that indicate the user consciously dismissed / acknowledged the dialog

    lastInteraction=$(grep -E '\[INFO\].*Return Code: (0|2|3|4|10)' "${scriptLog}" | \
        tail -1 | \
        sed -E 's/^[^:]+: ([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}).*/\1/')

    if (( ddmVersionStringDaysRemaining > daysBeforeDeadlineDisplayReminder )); then
        # Outside the deadline window; check if we should display initial/periodic reminder
        
        if [[ -z "${lastInteraction}" ]]; then
            # No interaction history; display the initial reminder dialog
            notice "No reminder interaction history found; displaying initial reminder dialog"
        else
            # Validate the extracted timestamp matches expected format
            if [[ "${lastInteraction}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}\ [0-9]{2}:[0-9]{2}:[0-9]{2}$ ]]; then
                nowEpoch=$(date +%s)
                lastEpoch=$( date -j -f "%Y-%m-%d %H:%M:%S" "${lastInteraction}" +"%s" 2>/dev/null )
                if [[ -n "${lastEpoch}" ]]; then
                    delta=$(( nowEpoch - lastEpoch ))
                    if (( delta >= periodicReminderSeconds )); then
                        # Last interaction was 28+ days ago; display periodic reminder
                        daysAgo=$(( delta / 86400 ))
                        notice "Last reminder interaction was ${daysAgo} day(s) ago; displaying periodic reminder dialog"
                    else
                        # Last interaction was within 28 days; skip
                        daysAgo=$(( delta / 86400 ))
                        quitOut "Deadline still ${ddmVersionStringDaysRemaining} days away and last reminder was ${daysAgo} day(s) ago; exiting quietly."
                        quitScript "0"
                    fi
                else
                    info "Could not parse last interaction timestamp; proceeding with display"
                fi
            else
                info "Last interaction timestamp format invalid; proceeding with display"
            fi
        fi
    else
        notice "Within ${daysBeforeDeadlineDisplayReminder}-day reminder window; proceeding …"
    fi

    # -------------------------------------------------------------------------
    # Short quiet period: skip dialog if user interacted very recently
    # -------------------------------------------------------------------------

    if [[ -n "${lastInteraction}" ]]; then
        # Validate the extracted timestamp matches expected format
        if [[ "${lastInteraction}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}\ [0-9]{2}:[0-9]{2}:[0-9]{2}$ ]]; then
            nowEpoch=$(date +%s)
            lastEpoch=$( date -j -f "%Y-%m-%d %H:%M:%S" "${lastInteraction}" +"%s" 2>/dev/null )
            if [[ -n "${lastEpoch}" ]]; then
                delta=$(( nowEpoch - lastEpoch ))
                if (( delta < quietPeriodSeconds )); then
                    minutesAgo=$(( delta / 60 ))
                    quitOut "User last interacted with reminder dialog ${minutesAgo} minute(s) ago; exiting quietly."
                    quitScript "0"
                fi
            fi
        fi
    fi



    # -------------------------------------------------------------------------
    # Confirm the currently logged-in user is “available” to be reminded
    # -------------------------------------------------------------------------

    if [[ "${ddmVersionStringDaysRemaining}" -gt 1 ]]; then
        if checkUserDisplaySleepAssertions; then
            notice "No active Display Sleep Assertions detected; proceeding …"
        else
            quitOut "Presentation still active after ${meetingDelay} minutes; exiting quietly."
            quitScript "0"
        fi
    else
        info "Deadline is within 24 hours; ignoring ${loggedInUser}’s Display Sleep Assertions; proceeding …"
    fi


    # -------------------------------------------------------------------------
    # Random pause depending on launch context (hourly vs login)
    # -------------------------------------------------------------------------

    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}"



    # -------------------------------------------------------------------------
    # Continue with normal processing
    # -------------------------------------------------------------------------

    updateRequiredVariables
    displayReminderDialog --ontop

else

    notice "Version Comparison Result: ${versionComparisonResult}"

fi



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

quitScript "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
2.2.0 (and later)

Version 3.0.0 of assemble.zsh can import supported settings from a previously generated DDM OS Reminder .plist, infer the RDNN and deployment lane (dev, test, prod), and generate a matched assembled script, organizational .plist, and unsigned .mobileconfig in a single pass.

zsh assemble.zsh drag-and-drop prior .plist
zsh assemble.zsh '/Users/dan/Downloads/DDM-OS-Reminder-2.2.0/Artifacts/us.snelson.dorm-2026-01-06-073608.plist'

===============================================================
🧩 Assemble DDM OS Reminder (3.0.0)
===============================================================

Full Paths:

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

🔍 Checking Reverse Domain Name Notation …

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

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


📥 Prior plist provided via command-line argument: '/Users/dan/Downloads/DDM-OS-Reminder-2.2.0/Artifacts/us.snelson.dorm-2026-01-06-073608.plist'

ℹ️  Importing supported values from: /Users/dan/Downloads/DDM-OS-Reminder-2.2.0/Artifacts/us.snelson.dorm-2026-01-06-073608.plist
🔎 Inferred RDNN from prior plist: 'us.snelson'

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


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Interactive Configuration
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


ℹ️  Prior plist supplied; skipping IT support, branding and restart policy prompts.


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Select Deployment Mode:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  1) Development - Keep placeholder text for local testing
  2) Testing     - Replace placeholder text with 'TEST' for staging
  3) Production  - Remove placeholder text for clean deployment

  [Press ‘X’ to exit]

Enter mode [1/2/3]: 3

📦 Deployment Mode: prod

🔧 Inserting reminderDialog.zsh into launchDaemonManagement.zsh  …

✅ Assembly complete [2026-03-28-151200]
   → Artifacts/ddm-os-reminder-assembled-2026-03-28-151200.zsh

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

🔍 Performing syntax check on 'Artifacts/ddm-os-reminder-assembled-2026-03-28-151200.zsh' …
    ✅ Syntax check passed.

🗂  Generating LaunchDaemon plist …
    🗂  Creating us.snelson.dorm plist from /Users/dan/Documents/GitHub/dan-snelson/DDM-OS-Reminder/Resources/sample.plist …

    🔧 Updating internal plist content …
    🔓 Production mode: removing placeholder text for clean deployment
    🔧 Importing supported values from prior plist …
    ℹ️  Preserving imported ScriptLog: /var/log/us.snelson.log
   → Artifacts/us.snelson.dorm-2026-03-28-151200-prod.plist

🧩 Generating Configuration Profile (.mobileconfig) …
   → Artifacts/us.snelson.dorm-2026-03-28-151200-prod-unsigned.mobileconfig

🔍 Performing syntax check on 'Artifacts/us.snelson.dorm-2026-03-28-151200-prod-unsigned.mobileconfig' …
    ✅ Profile syntax check passed.

🔁 Renaming assembled script …

🔁 Updating scriptLog path based on RDNN …

🏁 Done.

Deployment Artifacts:
        Assembled Script: Artifacts/ddm-os-reminder-us.snelson-2026-03-28-151200-prod.zsh
    Organizational Plist: Artifacts/us.snelson.dorm-2026-03-28-151200-prod.plist
   Configuration Profile: Artifacts/us.snelson.dorm-2026-03-28-151200-prod-unsigned.mobileconfig

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚠️  Important Next Steps:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  Production Artifacts Generated:
    - All placeholder text removed (clean output)
    - Supported configuration values imported from prior plist
    - Prior plist: /Users/dan/Downloads/DDM-OS-Reminder-2.2.0/Artifacts/us.snelson.dorm-2026-01-06-073608.plist
    - ScriptLog resolved to '/var/log/us.snelson.log'

  Recommended review items:
    - Support team name, phone, email, website
    - Imported ScriptLog path and any carried-forward KB/help visibility
    - Organization overlay icon URLs
    - Button labels and dialog messages

  Files to review:
    - Artifacts/us.snelson.dorm-2026-03-28-151200-prod.plist
    - Artifacts/us.snelson.dorm-2026-03-28-151200-prod-unsigned.mobileconfig

===============================================================

As always, carefully review each deployment artifact and distribute the appropriate files to a single test Mac via your MDM. (See Steps 2.2 through 2.4.)

2.1.0 (and earlier)

Author’s Highly Opinionated Thought: There are enough changes in version 2.1.0 and prior 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
5.1 AI-generated Documentation

AI-generated documentation is available in the GitHub repository

Diagrams: Comprehensive, AI-generated visual diagrams to augment this documentation

  1. Executive Overview: High-level lifecycle view for Mac Admins who need the big picture first
  2. System Architecture: Complete ecosystem overview from development through runtime execution
  3. Runtime Decision Tree: Complete decision logic executed each time the LaunchDaemon triggers
  4. Deadline Timeline: Visual representation of how user experience changes as deadline approaches
  5. Deployment Workflow: Step-by-step guide for administrators deploying DDM OS Reminder
  6. Configuration Hierarchy: 3-tier preference system showing precedence rules
  7. Configuration Reference: Complete reference for all 73 configurable preferences
5.2 Scripts

A number of Mac Admin quality-of-life scripts are available in the GitHub repository

Scripts

  1. The assemble.zsh script creates combined, deployable artifacts of your customized scripts
  2. Create Self-extracting encodes the most recently assembled script for easier deployment with some MDMs (real-world testing and feedback welcome)
  3. Create .plist creates .plist and .mobileconfig, based on your customizations to reminderDialog.zsh (real-world testing and feedback welcome)
  4. Extension Attributes were created for and tested on Jamf Pro and can most likely be adapted for other MDMs
  5. Jamf-getDDMstatusFromCSV.zsh; See: DDM Status from .CSV (1.3.0)
5.3 Configuration Profile

Special thanks to Max Sundell for his write-up

Create and deploy a macOS configuration profile (.mobileconfig)

5.4 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:

###
# Force-display reminder dialog
###

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



###
# Atomic Log Reset
###

lines=24

keep_lines=$(($(wc -l < "/var/log/${rdnn}.log") - lines))

sed -i '' "${keep_lines}q" "/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"
5.5 Troubleshooting

Conduct the following troubleshooting steps in an elevated Terminal session, on a test Mac in-scope of a pending Declarative Device Management-enforced macOS update from your MDM server.)

System Settings > General > Device Management > MDM Profile >Device Declarations

1. Core Functionality

  1. Define a rdnn variable to aid in completing the following steps:
rdnn="org.churchofjesuschrist"
  1. Review the client-side LaunchDaemon for any obvious issues:
plutil -p /Library/LaunchDaemons/"${rdnn}.dor.plist"

launchctl print system/"${rdnn}.dor" 2>/dev/null | grep -E 'state =|last exit code =|program =|path ='
  1. Confirm the existence of the client-side script:
ls -l /Library/Management/"${rdnn}"/dor.zsh
  1. Review the client-side managed preferences .plist for any obvious issues:
plutil -lint /Library/Managed\ Preferences/"${rdnn}".dorm.plist

plutil -p /Library/Managed\ Preferences/"${rdnn}".dorm.plist
  1. Review the script’s log for warnings and errors:
scriptLog=$(/usr/libexec/PlistBuddy -c 'Print :ScriptLog' "/Library/Managed Preferences/${rdnn}.dorm.plist" 2>/dev/null || /usr/libexec/PlistBuddy -c 'Print :ScriptLog' "/Library/Preferences/${rdnn}.dorm.plist" 2>/dev/null || echo "/var/log/${rdnn}.log")

grep -nE '^(dor|dorm) \([^)]*\): .* \[(WARNING|ERROR|FATAL ERROR)\]' "${scriptLog}"
  1. Review the /var/log/install.log for relevant DDM and Software Update entries:
    • Space : next page
    • Arrow keys : scroll horizontally or vertically
    • q : quit
tail -n 1000 /var/log/install.log | grep -nE 'declarationFromKeys\]: Falling back to default applicable declaration|Found DDM enforced install \(|EnforcedInstallDate:|requestedPMV=|MADownloadNoMatchFound|pallasNoPMVMatchFound=true|No available updates found\. Please try again later\.|setPastDuePaddedEnforcementDate|Removed [0-9]+ invalid declarations' | less -S
  1. To more easily validate the 3.1.0 DDM declaration parsing logic, run the following Jamf Pro Extension Attribute scripts locally and confirm the reported date and version match the pending DDM-enforced macOS update from your MDM server:
root@XDT8675309 ~ # zsh ~/Downloads/JamfEA-Pending_OS_Update_Date.zsh
<result>2026-03-31 18:00:00</result>

root@XDT8675309 ~ # zsh ~/Downloads/JamfEA-Pending_OS_Update_Version.zsh
<result>26.4</result>

When troubleshooting user-interface issues, leverage the dialog binary and your client-side managed preferences .plist values.

2. User-interface Issues

  1. Confirm dialog is installed and working as expected:
dialog --title "Dialog Test" --message "swiftDialog is installed and can render a basic dialog." --icon /System/Library/CoreServices/Finder.app --infotext
Confirm dialog is installed and working as expected: dialog --title "Dialog Test" --message "swiftDialog is installed and can render a basic dialog." --icon /System/Library/CoreServices/Finder.app --infotext
  1. Define a rdnn variable to aid in completing the following steps:
rdnn="org.churchofjesuschrist"
  1. See: Preference Preview and download reminderDialogPreferenceTest.zsh
zsh ~/Downloads/reminderDialogPreferenceTest.zsh --rdnn "${rdnn}"
See: Preference Preview and download reminderDialogPreferenceTest.zsh

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