1. Architecture: Enterprise Shell Scripting on Apple Silicon & macOS Darwin
Unlike standard Linux server environments where Bash or systemd reign supreme, macOS enterprise administration operates under Apple's Darwin kernel (XNU) and the Signed System Volume (SSV) security model. Since macOS 10.15 Catalina, Apple has designated /bin/zsh as the primary system shell, relegating /bin/bash to an obsolete 3.2.57 release due to GPLv3 licensing constraints.
Writing bulletproof automation scripts for managed fleets (deployed through Jamf Pro, Kandji, Addigy, or Microsoft Intune) introduces unique challenges that every Apple SysAdmin must navigate:
- Execution Context (Root vs Console User): Scripts pushed from MDM daemons execute under the
rootdaemon context (UID 0). Writing preferences or inspecting application settings meant for the logged-in user requires resolving the active GUI user (CONSOLE_USER=$(stat -f "%Su" /dev/console)) and executing commands viasudo -u "$CONSOLE_USER"or LaunchAgents. - The cfprefsd Caching Daemon: macOS caches configuration plists in memory via the
cfprefsdsubsystem. Directly editing.plistfiles on disk with text tools or PlistBuddy will often be overwritten or ignored unless routed through the officialdefaultscommand or by cyclingkillall cfprefsd. - System Integrity Protection (SIP) & TCC: Modern macOS strictly forbids automated scripting from writing to
/System,/usr/bin, or accessing protected user privacy silos (Desktop, Documents, Mail, Contacts) without explicit Transparency, Consent, and Control (TCC) MDM configuration profiles.
2. Script 1: Fleet-Wide Enterprise Health & Diagnostics Audit
Below is a production-tested audit script designed for fleet maintenance. It gathers hardware thermal metrics, battery degradation, FileVault 2 encryption verification, and disk capacity health without triggering invasive user interruptions:
#!/bin/zsh
# ==============================================================================
# Enterprise macOS Health Diagnostic & Compliance Check
# Target OS: macOS 13 Ventura, 14 Sonoma, 15 Sequoia (Apple Silicon & Intel)
# ==============================================================================
echo "=== MAC HEALTH REPORT: $(date) ==="
# 1. Resolve Active Logged-in Console User
CURRENT_USER=$(stat -f "%Su" /dev/console)
if [[ "$CURRENT_USER" == "root" || -z "$CURRENT_USER" ]]; then
echo "[!] No active GUI console session detected."
else
echo "[*] Active Console User: $CURRENT_USER"
fi
# 2. FileVault 2 Encryption & APFS Container Verification
FV_STATUS=$(fdesetup status)
if [[ "$FV_STATUS" == *"FileVault is On."* ]]; then
echo "[✓] FileVault 2 Status: Compliant (Enabled)"
else
echo "[CRITICAL] FileVault 2 Status: Non-Compliant (Disabled)"
fi
# 3. Disk Space Health & APFS Purgeable Inspection
FREE_SPACE_KB=$(df -k / | awk 'NR==2 {print $4}')
FREE_SPACE_GB=$(( FREE_SPACE_KB / 1024 / 1024 ))
echo "[*] Available Disk Space on Root: ${FREE_SPACE_GB} GB"
if (( FREE_SPACE_GB < 20 )); then
echo "[WARNING] Low Disk Space alert: < 20 GB remaining!"
fi
# 4. Battery Cycle Count & Health (Applicable to MacBooks)
if system_profiler SPPowerDataType | grep -q "Battery Information"; then
CYCLES=$(system_profiler SPPowerDataType | awk -F': ' '/Cycle Count/ {print $2}' | xargs)
CONDITION=$(system_profiler SPPowerDataType | awk -F': ' '/Condition/ {print $2}' | xargs)
echo "[*] Battery Health: ${CONDITION:-Normal} (Cycles: ${CYCLES:-N/A})"
fi
# 5. Thermal Throttling Status
THERMAL_STATE=$(pmset -g therm | grep "CPU_Speed_Limit" | awk '{print $3}')
echo "[*] CPU Speed Limit: ${THERMAL_STATE:-100}%"
echo "=== Diagnostic Run Complete ==="
exit 0
3. Script 2: Zero-Touch Workstation Provisioning & Preference Tuning
When onboarding new Mac laptops, systems engineers use shell automation to configure global operating system defaults, suppress annoying prompts, and optimize Finder behavior:
#!/bin/zsh
# ==============================================================================
# Enterprise User Environment Baseline Provisioning
# ==============================================================================
CONSOLE_USER=$(stat -f "%Su" /dev/console)
USER_HOME=$(dscl . -read /Users/"$CONSOLE_USER" NFSHomeDirectory | awk '{print $2}')
if [[ "$CONSOLE_USER" == "root" || -z "$CONSOLE_USER" ]]; then
echo "No console user found; exiting."
exit 0
fi
echo "Configuring preferences for user: $CONSOLE_USER"
# 1. Prevent .DS_Store file creation on enterprise network SMB shares
sudo -u "$CONSOLE_USER" defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true
sudo -u "$CONSOLE_USER" defaults write com.apple.desktopservices DSDontWriteUSBStores -bool true
# 2. Show file extensions, path bar, and status bar in Finder
sudo -u "$CONSOLE_USER" defaults write NSGlobalDomain AppleShowAllExtensions -bool true
sudo -u "$CONSOLE_USER" defaults write com.apple.finder ShowPathbar -bool true
sudo -u "$CONSOLE_USER" defaults write com.apple.finder ShowStatusBar -bool true
# 3. Disable automatic spelling correction and capitalization
sudo -u "$CONSOLE_USER" defaults write NSGlobalDomain NSAutomaticSpellingCorrectionEnabled -bool false
sudo -u "$CONSOLE_USER" defaults write NSGlobalDomain NSAutomaticCapitalizationEnabled -bool false
# 4. Set fast key repeat rates for developers
sudo -u "$CONSOLE_USER" defaults write NSGlobalDomain KeyRepeat -int 2
sudo -u "$CONSOLE_USER" defaults write NSGlobalDomain InitialKeyRepeat -int 15
# 5. Flush cfprefsd and restart Finder and Dock
killall cfprefsd
killall Finder
killall Dock
echo "[✓] Enterprise defaults successfully enforced."
4. Script 3: Cryptographic Token & SecureToken Audit
On macOS Sonoma and Sequoia, disk encryption and software updates on Apple Silicon require that accounts have a valid SecureToken and that the enterprise MDM maintains an escrowed Bootstrap Token:
# Check SecureToken status for currently logged-in user
sysadminctl -secureTokenStatus $(stat -f "%Su" /dev/console)
# Check enterprise Bootstrap Token escrow status with MDM server
profiles status -type bootstraptoken
# Verify APFS cryptographic volume metadata
diskutil apfs listCryptoUsers /
5. Troubleshooting Common macOS Scripting Gotchas
| Issue / Error | Underlying Cause | Resolution |
|---|---|---|
defaults write runs without error but setting reverts immediately |
cfprefsd overwriting plist from memory cache |
Always issue killall cfprefsd immediately following plist writes or execute preferences as the console user. |
Script returns Operation not permitted when touching files |
Target directory protected by macOS System Integrity Protection (SIP) or TCC | Move custom scripts and binaries to /usr/local/bin or /Library/Application Support/. |
| Command fails on Apple Silicon Macs but succeeds on Intel | Hardcoded binary path (e.g. /usr/local/bin vs /opt/homebrew/bin) |
Use architecture detection: [[ $(uname -m) == "arm64" ]] and dynamically set tool paths. |
6. Frequently Asked Questions (FAQ)
Q: Why is Zsh preferred over Bash for macOS automation scripts?
A: Apple maintains /bin/zsh with modern features and security patches, whereas macOS ships with an ancient Bash 3.2 binary that lacks associative arrays, modern globbing, and POSIX compliance features.
Q: How do MDM agents like Jamf run shell scripts?
A: MDM client binaries (e.g. jamf, kandji-agent) execute scripts as root in non-interactive subshells. They pass standard parameters (Target Drive, Computer Name, Username) through positional parameters $1, $2, $3.
Q: What is the safest way to delete temporary files in macOS scripts?
A: Always validate target directory path variables before invoking rm -rf (e.g. check [ -n "$TARGET_DIR" ] && [ -d "$TARGET_DIR" ]) to prevent accidental root or user home directory wiping.