Created
March 1, 2026 11:24
-
-
Save lispstudent/6ff83b668019721d8958f9c0f6dc933e to your computer and use it in GitHub Desktop.
block-tahoe.sh — Block "Upgrade to Tahoe" alerts and System Settings indicator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # block-tahoe.sh — Block "Upgrade to Tahoe" alerts and System Settings indicator | |
| # Version: 1.2.0 — 2026-03-01 | |
| # | |
| # Creates and installs a macOS configuration profile that defers major OS upgrades | |
| # for 90 days (the maximum Apple allows via com.apple.applicationaccess). | |
| # | |
| # Based on: https://robservatory.com/block-the-upgrade-to-tahoe-alerts-and-system-settings-indicator/ | |
| # https://github.com/travisvn/stop-tahoe-update | |
| # | |
| # Payload reference: | |
| # PayloadType: com.apple.applicationaccess | |
| # Keys: forceDelayedMajorSoftwareUpdates (bool) | |
| # enforcedSoftwareUpdateMajorOSDeferredInstallDelay (int, 1-90) | |
| # forceDelayedSoftwareUpdates (bool) | |
| # See: https://developer.apple.com/documentation/devicemanagement/restrictions | |
| # | |
| # IMPORTANT: This works due to a bug in macOS 15.7.3 where the 90-day deferral | |
| # acts as a rolling window. Apple may fix this in 15.7.4+. Reinstall every 90 days. | |
| # | |
| # Usage: | |
| # ./block-tahoe.sh install — Create and install the deferral profile | |
| # ./block-tahoe.sh remove — Remove the installed profile (requires sudo) | |
| # ./block-tahoe.sh status — Check if profile is installed | |
| # | |
| # Requirements: macOS Sequoia 15.7.3+. | |
| set -euo pipefail | |
| # -- Configuration ------------------------------------------------------------ | |
| PROFILE_IDENTIFIER="com.robservatory.tahoe-blocker.deferral-90days" | |
| PROFILE_DISPLAY_NAME="Block Tahoe Upgrade (90-day deferral)" | |
| PROFILE_DIR="${HOME}/.config/block-tahoe" | |
| PROFILE_PATH="${PROFILE_DIR}/deferral-90days.mobileconfig" | |
| DEFERRAL_DAYS=90 | |
| # -- Helpers ------------------------------------------------------------------- | |
| usage() { | |
| cat <<EOF | |
| Usage: $(basename "$0") {install|remove|status} | |
| install — Generate profile and open for installation | |
| remove — Remove the deferral profile (requires sudo) | |
| status — Check whether the profile is currently installed | |
| Note: This defers MAJOR OS upgrades (Tahoe) for ${DEFERRAL_DAYS} days. | |
| Minor Sequoia updates are NOT deferred (configurable in profile). | |
| EOF | |
| exit 1 | |
| } | |
| require_macos() { | |
| if [[ "$(uname)" != "Darwin" ]]; then | |
| echo "Error: This script only runs on macOS." >&2 | |
| exit 1 | |
| fi | |
| } | |
| require_sudo() { | |
| if [[ $EUID -ne 0 ]]; then | |
| echo "Error: This command requires sudo privileges." >&2 | |
| echo " Try: sudo $(basename "$0") ${1:-}" >&2 | |
| exit 1 | |
| fi | |
| } | |
| generate_uuid() { | |
| uuidgen | tr '[:lower:]' '[:upper:]' | |
| } | |
| # -- Install ------------------------------------------------------------------- | |
| do_install() { | |
| local uuid_profile uuid_payload | |
| uuid_profile="$(generate_uuid)" | |
| uuid_payload="$(generate_uuid)" | |
| echo "Creating deferral profile..." | |
| echo " Profile UUID: ${uuid_profile}" | |
| echo " Payload UUID: ${uuid_payload}" | |
| echo " Identifier: ${PROFILE_IDENTIFIER}" | |
| echo " Deferral: ${DEFERRAL_DAYS} days for major OS upgrades only" | |
| echo "" | |
| mkdir -p "${PROFILE_DIR}" | |
| # PayloadType: com.apple.applicationaccess | |
| # This is the payload type used by the stop-tahoe-update project and | |
| # confirmed working on macOS 15.7.3 by multiple users. | |
| cat > "${PROFILE_PATH}" <<MOBILECONFIG | |
| <?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> | |
| <!-- Profile metadata --> | |
| <key>PayloadDisplayName</key> | |
| <string>${PROFILE_DISPLAY_NAME}</string> | |
| <key>PayloadDescription</key> | |
| <string>Defers major macOS upgrades (e.g. Tahoe) for ${DEFERRAL_DAYS} days. Minor updates are not affected. Reinstall every 90 days to maintain deferral.</string> | |
| <key>PayloadIdentifier</key> | |
| <string>${PROFILE_IDENTIFIER}</string> | |
| <key>PayloadOrganization</key> | |
| <string>Local Admin</string> | |
| <key>PayloadType</key> | |
| <string>Configuration</string> | |
| <key>PayloadUUID</key> | |
| <string>${uuid_profile}</string> | |
| <key>PayloadVersion</key> | |
| <integer>1</integer> | |
| <key>PayloadRemovalDisallowed</key> | |
| <false/> | |
| <!-- Payloads array --> | |
| <key>PayloadContent</key> | |
| <array> | |
| <dict> | |
| <key>PayloadDisplayName</key> | |
| <string>Software Update Deferral</string> | |
| <key>PayloadIdentifier</key> | |
| <string>${PROFILE_IDENTIFIER}.restrictions</string> | |
| <key>PayloadType</key> | |
| <string>com.apple.applicationaccess</string> | |
| <key>PayloadUUID</key> | |
| <string>${uuid_payload}</string> | |
| <key>PayloadVersion</key> | |
| <integer>1</integer> | |
| <!-- Defer major OS upgrades for 90 days (max allowed) --> | |
| <key>forceDelayedMajorSoftwareUpdates</key> | |
| <true/> | |
| <key>enforcedSoftwareUpdateMajorOSDeferredInstallDelay</key> | |
| <integer>${DEFERRAL_DAYS}</integer> | |
| <!-- Do NOT defer minor/security updates --> | |
| <key>forceDelayedSoftwareUpdates</key> | |
| <false/> | |
| </dict> | |
| </array> | |
| </dict> | |
| </plist> | |
| MOBILECONFIG | |
| echo "Profile written to: ${PROFILE_PATH}" | |
| echo "" | |
| echo "IMPORTANT — macOS 15.7.3 behavior notice:" | |
| echo " This deferral works as a rolling 90-day window due to a bug in" | |
| echo " macOS 15.7.3. Normally, deferrals count from the OS release date." | |
| echo " If Apple fixes this in 15.7.4+, the profile may stop working." | |
| echo " Reinstall every ~90 days to maintain the block." | |
| echo "" | |
| echo "Opening profile for installation..." | |
| echo "" | |
| echo "Complete these steps in System Settings:" | |
| echo " 1. Click 'Profile Downloaded' in the sidebar" | |
| echo " 2. Double-click '${PROFILE_DISPLAY_NAME}'" | |
| echo " 3. Click 'Install', confirm with password, click 'Install' again" | |
| echo " 4. Quit and relaunch System Settings to verify" | |
| echo " Software Update should show the deferral message." | |
| echo "" | |
| # The 'profiles' CLI no longer supports installs on Sequoia; | |
| # 'open' triggers the System Settings installation flow instead. | |
| open "${PROFILE_PATH}" | |
| sleep 2 | |
| open "x-apple.systempreferences:com.apple.preferences.configurationprofiles" 2>/dev/null || \ | |
| open "x-apple.systempreferences:com.apple.preferences.profiles" 2>/dev/null || true | |
| echo "System Settings opened. Complete installation manually." | |
| echo "Set a reminder to run '$(basename "$0") install' again in ~90 days." | |
| } | |
| # -- Remove -------------------------------------------------------------------- | |
| do_remove() { | |
| require_sudo "remove" | |
| echo "Searching for installed profile: ${PROFILE_IDENTIFIER}..." | |
| echo "" | |
| local profile_info | |
| profile_info=$(profiles -P 2>/dev/null | grep -B 10 -A 2 "${PROFILE_IDENTIFIER}" || true) | |
| if [[ -z "${profile_info}" ]]; then | |
| echo "Profile not found via CLI. It may not be installed." | |
| echo "" | |
| echo "To check/remove manually:" | |
| echo " System Settings > Privacy & Security > Profiles" | |
| echo " Select '${PROFILE_DISPLAY_NAME}' and click Remove." | |
| open "x-apple.systempreferences:com.apple.preferences.configurationprofiles" 2>/dev/null || true | |
| cleanup_local_file | |
| return 0 | |
| fi | |
| local payload_uuid | |
| payload_uuid=$(echo "${profile_info}" | grep "PayloadUUID" | head -1 | awk -F': ' '{print $2}' | tr -d ' "<>') | |
| if [[ -z "${payload_uuid}" ]]; then | |
| echo "Could not extract profile UUID. Falling back to manual removal." | |
| echo " System Settings > Privacy & Security > Profiles" | |
| open "x-apple.systempreferences:com.apple.preferences.configurationprofiles" 2>/dev/null || true | |
| cleanup_local_file | |
| return 0 | |
| fi | |
| echo "Removing profile (UUID: ${payload_uuid})..." | |
| if profiles -D -p "${payload_uuid}" 2>/dev/null; then | |
| echo "Profile successfully removed." | |
| else | |
| echo "CLI removal failed. Please remove manually:" | |
| echo " System Settings > Privacy & Security > Profiles" | |
| echo " Select '${PROFILE_DISPLAY_NAME}' and click Remove." | |
| open "x-apple.systempreferences:com.apple.preferences.configurationprofiles" 2>/dev/null || true | |
| fi | |
| echo "" | |
| echo "After removal, Tahoe upgrade notifications will resume." | |
| cleanup_local_file | |
| } | |
| cleanup_local_file() { | |
| if [[ -f "${PROFILE_PATH}" ]]; then | |
| echo "" | |
| read -r -p "Delete local profile file '${PROFILE_PATH}'? [y/N] " answer | |
| case "${answer}" in | |
| [yY]|[yY][eE][sS]) | |
| rm -f "${PROFILE_PATH}" | |
| rmdir "${PROFILE_DIR}" 2>/dev/null || true | |
| echo "Local profile file deleted." | |
| ;; | |
| *) | |
| echo "Local file retained at: ${PROFILE_PATH}" | |
| ;; | |
| esac | |
| fi | |
| } | |
| # -- Status -------------------------------------------------------------------- | |
| do_status() { | |
| echo "Checking for installed deferral profile..." | |
| echo "" | |
| if profiles -P 2>/dev/null | grep -q "${PROFILE_IDENTIFIER}"; then | |
| echo "INSTALLED — Profile is active." | |
| echo " Identifier: ${PROFILE_IDENTIFIER}" | |
| echo "" | |
| echo "Profile details:" | |
| profiles -P 2>/dev/null | grep -A 8 "${PROFILE_IDENTIFIER}" | head -10 || true | |
| echo "" | |
| echo "To verify: System Settings > General > Software Update" | |
| echo "You should see a message about deferred major updates." | |
| else | |
| echo "NOT INSTALLED — Profile is not active." | |
| echo "" | |
| echo "Run '$(basename "$0") install' to create and install it." | |
| fi | |
| echo "" | |
| if [[ -f "${PROFILE_PATH}" ]]; then | |
| echo "Local profile file: ${PROFILE_PATH}" | |
| echo " (Reinstall with: open \"${PROFILE_PATH}\")" | |
| else | |
| echo "No local profile file found." | |
| fi | |
| } | |
| # -- Main ---------------------------------------------------------------------- | |
| require_macos | |
| case "${1:-}" in | |
| install) do_install ;; | |
| remove) do_remove ;; | |
| status) do_status ;; | |
| *) usage ;; | |
| esac |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I can confirm this works on Mac Studio 2025