1. Architecture: The Jamf Binary vs Apple MDM Framework
Enterprise Apple device management operates across two parallel channels:
- The Native Apple MDM Protocol: Driven by the macOS kernel and Apple Push Notification service (APNs). MDM profiles, Declarative Device Management (DDM), and security commands (e.g. Remote Lock) execute via the native system daemon
/usr/libexec/mdmclientwithout requiring third-party software. - The Jamf Management Framework (jamf binary): A privileged root agent installed at
/usr/local/jamf/bin/jamf. The binary handles tasks outside the native MDM specification: package installation, recurring policy execution, local user creation, self-service catalog management, and extended inventory reporting (jamf recon).
Because Smart Computer Groups rely on up-to-date inventory attributes (such as OS build, disk space, and FileVault recovery keys), stale inventory records can lead to policy delivery delays or false security alerts. While Jamf Pro offers a scheduled daily or weekly inventory update trigger in policies, laptops that frequently change hands or run remote testing often boot up without immediately reporting new hardware states.
2. The Network Race Condition at Boot Time
A frequent error when executing boot-time LaunchDaemons is the network availability race condition. When macOS boots to the loginwindow, root LaunchDaemons execute before the user authenticates. If the endpoint is connected via Wi-Fi (especially 802.1X enterprise networks that require user credentials) or is behind a captive portal, an immediate jamf recon invocation will fail with an HTTP network timeout.
To overcome this, the script leverages the built-in Jamf connection validator: jamf checkJSSConnection -retry 60. This command pauses execution until DNS resolves and the Jamf Pro server responds, retrying every second for up to 60 seconds before executing the inventory collection.
3. Production Script: Automated Startup Inventory LaunchDaemon
This production shell script generates, secures, and bootstraps a system-level LaunchDaemon using modern launchctl bootstrap system syntax (adapted from Shri Sivakumaran and Rich Trouton):
#!/bin/sh
# ==============================================================================
# Script: startup_inventory_update_jamf.sh
# Author: Shri Sivakumaran C (MacTechBuds) / Adapted from rtrouton
# Target: macOS 11 Big Sur through macOS 15 Sequoia (Apple Silicon & Intel)
# Purpose: Creates a LaunchDaemon to trigger 'jamf recon' after network verify
# ==============================================================================
LaunchDaemonName="com.company.runjamfproinventoryupdate"
ERROR=0
# 1. Create secure temporary working directory
temp_directory=$(mktemp -d)
# 2. Generate LaunchDaemon plist configuration
/bin/cat > "$temp_directory/$LaunchDaemonName.plist" << JAMF_PRO_INVENTORY_UPDATE_LAUNCHDAEMON
Label
$LaunchDaemonName
ProgramArguments
sh
-c
/usr/local/jamf/bin/jamf checkJSSConnection -retry 60 && /usr/local/jamf/bin/jamf recon
RunAtLoad
JAMF_PRO_INVENTORY_UPDATE_LAUNCHDAEMON
# 3. Secure file permissions (root:wheel, 644 non-executable)
/usr/sbin/chown root:wheel "${temp_directory}/${LaunchDaemonName}.plist"
/bin/chmod 644 "${temp_directory}/${LaunchDaemonName}.plist"
/bin/chmod a-x "${temp_directory}/${LaunchDaemonName}.plist"
/bin/mv "${temp_directory}/${LaunchDaemonName}.plist" "/Library/LaunchDaemons/${LaunchDaemonName}.plist"
# 4. Bootstrap LaunchDaemon using modern launchctl subsystem
if [[ -f "/Library/LaunchDaemons/${LaunchDaemonName}.plist" ]]; then
# Unload legacy instances if present
/bin/launchctl bootout system "/Library/LaunchDaemons/${LaunchDaemonName}.plist" 2>/dev/null
# Bootstrap new service
/bin/launchctl bootstrap system "/Library/LaunchDaemons/${LaunchDaemonName}.plist"
fi
# 5. Clean up temporary directory
/bin/rm -rf "$temp_directory"
# 6. Verify LaunchDaemon registration
if [[ -f "/Library/LaunchDaemons/${LaunchDaemonName}.plist" ]]; then
LaunchDaemonLoaded=$(/bin/launchctl print system/$LaunchDaemonName 2>/dev/null)
if [[ -n "$LaunchDaemonLoaded" ]]; then
echo "[+] $LaunchDaemonName LaunchDaemon loaded successfully. Inventory will run at boot."
else
echo "[-] ERROR: $LaunchDaemonName is not active in system domain."
ERROR=1
fi
else
echo "[-] ERROR: Failed to create LaunchDaemon file."
ERROR=1
fi
exit "$ERROR"
4. Modern macOS launchctl: bootstrap vs load
Historically, macOS administrators used launchctl load -w /Library/LaunchDaemons/.... However, Apple deprecated load and unload in favor of the modern domain-targeted subcommands:
launchctl bootstrap system <path>: Loads and activates a daemon directly into the privileged system domain (UID 0).launchctl bootout system <path>: Gracefully terminates running processes and unregisters the service identifier fromlaunchd.launchctl print system/<label>: Inspects PID, exit status, memory limits, and environment paths of the running daemon.
5. Troubleshooting Matrix: Jamf Inventory Issues
| Symptom | Root Cause | Recommended Remediation |
|---|---|---|
| Boot inventory update hangs or never reports to Jamf Pro | Device on guest Wi-Fi requiring web portal authentication before internet routing is granted | The -retry 60 timeout safely aborts after 60 seconds. Add a recurring policy check-in to catch up once user signs into captive portal. |
launchctl bootstrap returns error 125: Domain does not support specified action |
Script executed in user context rather than root, or plist has incorrect wheel ownership | Ensure script is run with sudo or deployed via Jamf policy running as root; verify file permissions are root:wheel 644. |
| Inventory report missing active console user username | jamf recon executed before user logged in at the loginwindow screen |
Trigger a secondary lightweight recon policy upon user login using Jamf's native Login trigger. |
6. Frequently Asked Questions (FAQ)
Q: Does running jamf recon at every boot slow down Mac startup?
A: No. LaunchDaemons execute asynchronously in the background. The user sees no delay or spinning wheel on the login screen while Jamf collects inventory metrics.
Q: Can I limit how frequently the inventory update is sent to save server resources?
A: Yes. You can wrap the recon command in a check that queries the timestamp of /Library/Application Support/JAMF/Receipts and skips the scan if an update occurred within the last 4 hours.