Skip to content

Instantly share code, notes, and snippets.

@lispstudent
Created March 1, 2026 11:24
Show Gist options
  • Select an option

  • Save lispstudent/6ff83b668019721d8958f9c0f6dc933e to your computer and use it in GitHub Desktop.

Select an option

Save lispstudent/6ff83b668019721d8958f9c0f6dc933e to your computer and use it in GitHub Desktop.
block-tahoe.sh — Block "Upgrade to Tahoe" alerts and System Settings indicator
#!/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
@lispstudent
Copy link
Author

I can confirm this works on Mac Studio 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment