safe-disable-startup.sh 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. #!/usr/bin/env bash
  2. # mac-ops :: safe-disable-startup.sh
  3. # Disable a startup item by name pattern. Reversible.
  4. #
  5. # Mechanisms handled (no sudo for user-scope):
  6. # - Login Items (via osascript / System Events)
  7. # - User LaunchAgents (launchctl disable gui/$UID/<label>)
  8. # - System LaunchAgents (launchctl disable gui/$UID/<label>)
  9. #
  10. # Mechanisms handled (sudo required):
  11. # - System LaunchDaemons (sudo launchctl disable system/<label>)
  12. #
  13. # Default mode is DRY RUN. Pass --apply to actually disable.
  14. # Use --enable to reverse a prior disable.
  15. set -u
  16. NAME_PATTERN=""
  17. APPLY=0
  18. ENABLE=0
  19. LIST_ONLY=0
  20. while [[ $# -gt 0 ]]; do
  21. case "$1" in
  22. -n|--name) NAME_PATTERN="$2"; shift 2 ;;
  23. --list) LIST_ONLY=1; shift ;;
  24. --apply) APPLY=1; shift ;;
  25. --enable) ENABLE=1; APPLY=1; shift ;;
  26. --help|-h)
  27. cat <<EOF
  28. Usage: $0 [options]
  29. -n, --name PATTERN Glob pattern matching the entry label / name
  30. --list List all currently-disabled launchd entries
  31. --apply Actually perform the disable (default: dry-run)
  32. --enable Re-enable a previously disabled item (implies --apply)
  33. --json, --redact, --quiet, --verbose Standard flags
  34. Examples:
  35. $0 --list # show current disable state
  36. $0 -n 'com.adobe.*' # dry-run: what would be disabled?
  37. $0 -n 'com.adobe.*' --apply # disable Adobe agents
  38. $0 -n 'com.adobe.*' --enable # re-enable
  39. $0 -n 'Adobe Updater' --apply # also matches Login Item by name
  40. Note: System LaunchDaemons (/Library/LaunchDaemons) require sudo and operate
  41. on system/<label> instead of gui/\$UID/<label>. The script asks for sudo only
  42. when needed.
  43. EOF
  44. exit 0 ;;
  45. *) shift ;;
  46. esac
  47. done
  48. source "$(dirname "$0")/_lib/common.sh"
  49. parse_common_flags "$@"
  50. maybe_filter_self "$@"
  51. # ----------------------------------------------------------------------------
  52. # --list mode: show all disabled launchctl entries
  53. # ----------------------------------------------------------------------------
  54. if [[ "$LIST_ONLY" -eq 1 ]]; then
  55. section "DISABLED LAUNCHD ENTRIES (user domain)"
  56. if launchctl print-disabled "gui/$UID" 2>/dev/null | grep -E "=> (true|disabled)$" | sed 's/^/ /'; then
  57. :
  58. else
  59. note " (no user-domain disables, or print-disabled requires newer macOS)"
  60. fi
  61. section "DISABLED LAUNCHD ENTRIES (system domain — sudo)"
  62. if sudo -n launchctl print-disabled system 2>/dev/null | grep -E "=> (true|disabled)$" | sed 's/^/ /'; then
  63. :
  64. else
  65. note " (system domain requires sudo, or no entries disabled)"
  66. fi
  67. emit_summary
  68. exit 0
  69. fi
  70. if [[ -z "$NAME_PATTERN" ]]; then
  71. echo "Error: -n PATTERN required (or --list)" >&2
  72. exit "$EXIT_USAGE"
  73. fi
  74. # ----------------------------------------------------------------------------
  75. section "1. SEARCH MATCHES"
  76. # ----------------------------------------------------------------------------
  77. # Find matching LaunchAgent plists (user + system)
  78. user_matches=()
  79. sys_agent_matches=()
  80. sys_daemon_matches=()
  81. for p in "$HOME/Library/LaunchAgents"/*.plist /Library/LaunchAgents/*.plist; do
  82. [[ -f "$p" ]] || continue
  83. label=$(/usr/libexec/PlistBuddy -c "Print :Label" "$p" 2>/dev/null || basename "$p" .plist)
  84. # Match by label OR filename
  85. if [[ "$label" == $NAME_PATTERN ]] || [[ "$(basename "$p" .plist)" == $NAME_PATTERN ]]; then
  86. case "$p" in
  87. "$HOME"/*) user_matches+=("$label|$p") ;;
  88. *) sys_agent_matches+=("$label|$p") ;;
  89. esac
  90. fi
  91. done
  92. for p in /Library/LaunchDaemons/*.plist; do
  93. [[ -f "$p" ]] || continue
  94. label=$(/usr/libexec/PlistBuddy -c "Print :Label" "$p" 2>/dev/null || basename "$p" .plist)
  95. if [[ "$label" == $NAME_PATTERN ]] || [[ "$(basename "$p" .plist)" == $NAME_PATTERN ]]; then
  96. sys_daemon_matches+=("$label|$p")
  97. fi
  98. done
  99. # Find matching Login Items (by name only; AppleScript glob match)
  100. login_item_matches=()
  101. if items=$(osascript <<APPLESCRIPT 2>/dev/null
  102. tell application "System Events"
  103. set output to ""
  104. repeat with li in (every login item)
  105. set itemName to name of li
  106. if itemName is like "$NAME_PATTERN" then
  107. set output to output & itemName & linefeed
  108. end if
  109. end repeat
  110. return output
  111. end tell
  112. APPLESCRIPT
  113. ); then
  114. while IFS= read -r name; do
  115. [[ -n "$name" ]] && login_item_matches+=("$name")
  116. done <<< "$items"
  117. fi
  118. total_matches=$(( ${#user_matches[@]} + ${#sys_agent_matches[@]} + ${#sys_daemon_matches[@]} + ${#login_item_matches[@]} ))
  119. if [[ "$total_matches" -eq 0 ]]; then
  120. log_warn "Matches for '$NAME_PATTERN'" "0 — nothing to do"
  121. emit_summary
  122. exit "$EXIT_NOT_FOUND"
  123. fi
  124. log_pass "Matches for '$NAME_PATTERN'" "$total_matches"
  125. [[ ${#user_matches[@]} -gt 0 ]] && note " User LaunchAgents:" && printf " %s\n" "${user_matches[@]%|*}"
  126. [[ ${#sys_agent_matches[@]} -gt 0 ]] && note " System LaunchAgents:" && printf " %s\n" "${sys_agent_matches[@]%|*}"
  127. [[ ${#sys_daemon_matches[@]} -gt 0 ]] && note " System LaunchDaemons:" && printf " %s\n" "${sys_daemon_matches[@]%|*}"
  128. [[ ${#login_item_matches[@]} -gt 0 ]] && note " Login Items:" && printf " %s\n" "${login_item_matches[@]}"
  129. # ----------------------------------------------------------------------------
  130. if [[ "$APPLY" -eq 0 ]]; then
  131. section "2. DRY RUN — would $([[ "$ENABLE" -eq 1 ]] && echo enable || echo disable)"
  132. note " Pass --apply to perform the action."
  133. emit_summary
  134. exit 0
  135. fi
  136. # ----------------------------------------------------------------------------
  137. verb=$([[ "$ENABLE" -eq 1 ]] && echo enable || echo disable)
  138. section "2. APPLY — ${verb}"
  139. # launchctl verb selection
  140. lctl_verb=$([[ "$ENABLE" -eq 1 ]] && echo enable || echo disable)
  141. # Disable user agents (no sudo)
  142. for entry in ${user_matches[@]+"${user_matches[@]}"} ${sys_agent_matches[@]+"${sys_agent_matches[@]}"}; do
  143. label="${entry%|*}"
  144. if launchctl "$lctl_verb" "gui/$UID/$label" 2>/dev/null; then
  145. log_pass "launchctl $lctl_verb gui/$UID/$label"
  146. else
  147. log_warn "launchctl $lctl_verb gui/$UID/$label" "may already be in target state"
  148. fi
  149. done
  150. # Disable system daemons (sudo)
  151. if [[ ${#sys_daemon_matches[@]} -gt 0 ]]; then
  152. note " System daemons require sudo:"
  153. for entry in "${sys_daemon_matches[@]}"; do
  154. label="${entry%|*}"
  155. if sudo launchctl "$lctl_verb" "system/$label" 2>/dev/null; then
  156. log_pass "sudo launchctl $lctl_verb system/$label"
  157. else
  158. log_warn "sudo launchctl $lctl_verb system/$label" "may need sudo or already in state"
  159. fi
  160. done
  161. fi
  162. # Login Items (via osascript)
  163. for name in ${login_item_matches[@]+"${login_item_matches[@]}"}; do
  164. if [[ "$ENABLE" -eq 1 ]]; then
  165. log_warn "Login Item '$name'" "re-enable requires manual re-add (System Settings → Login Items)"
  166. else
  167. if osascript -e "tell application \"System Events\" to delete login item \"$name\"" 2>/dev/null; then
  168. log_pass "Removed Login Item" "$name"
  169. else
  170. log_warn "Login Item '$name'" "removal failed (TCC may be blocking System Events)"
  171. fi
  172. fi
  173. done
  174. emit_summary