Remote Off-boarding of Mac Devices from Jamf Pro Management

New Contributor III

Hi all,

Looking for advice with my 'Off-boarding Script'


This script (below) is crafted for system administrators utilizing Jamf Pro to facilitate the remote off-boarding of Mac devices that end users have purchased. It performs several functions to ensure the device is no longer managed or configured for enterprise use. The script automates the removal of management settings, promotes the current user to admin, creates backup accounts, and eliminates no longer necessary accounts and management software. Furthermore, it updates the device's details in Jamf Pro's inventory, sends essential information to a Google Sheet, and concludes by removing the Jamf management framework, making the process as hands-off as possible.



# Functions

removePolicyBanner() {
    if [ -e "/Library/Security/PolicyBanner.rtfd" ]; then
        echo "Found PolicyBanner... Removing PolicyBanner"
        rm -rf "/Library/Security/PolicyBanner.rtfd"
        echo "No PolicyBanner Installed"

removeNoMADLogin() {
    if [ -e "/usr/local/bin/authchanger" ]; then 
        echo "Found Authchanger... removing now"
        /usr/local/bin/authchanger -reset
        echo "No Authchanger"

unbindFromAD() {
    local result=$(dsconfigad -f -r -u "$1" -p "$2")
    if [ $? -ne 0 ]; then
        echo "Failed to unbind from AD: $result"
        #exit 1

promoteToAdmin() {
    local user="$1"
    if [[ $(/usr/bin/dscl . read /Groups/admin GroupMembership | /usr/bin/grep -c $user) == 1 ]]; then
        echo "$user is in the admin group."
        echo "$user is not an admin, promoting..."
        /usr/sbin/dseditgroup -o edit -a "$user" -t user admin

removeFortiClient() {
    if [ -e "/Applications/" ]; then
        echo "/Applications/FortiClient located; proceeding ..."
        echo "Removing FortiClient ..."
        rm -rf "/Library/Application Support/Fortinet/"
        echo "Removed /Applications/FortiClient."
        echo "/Applications/FortiClient NOT found; nothing to remove."

checkJamfBinary() {
    if which jamf > /dev/null 2>&1; then
        return 0
        return 1

deleteFirmwarePassword() {
    local primaryPassword="$1"
    local backupPassword="$2"

    /usr/bin/expect <<EOF
set primary "${primaryPassword}"
set backup "${backupPassword}"

spawn /usr/sbin/firmwarepasswd -check

expect {
    "Password Enabled: Yes" {
        spawn /usr/sbin/firmwarepasswd -delete
        expect "Enter password:"
        send -- "\$primary\r"
        expect {
            "ERROR | main | Exiting with error: 5" {
                puts "Primary password failed, trying backup."
                # Re-spawn the delete process for the backup password attempt
                spawn /usr/sbin/firmwarepasswd -delete
                expect "Enter password:"
                send -- "\$backup\r"
                expect {
                    "ERROR | main | Exiting with error: 5" {
                        puts "Backup password also failed."
                    -re "#" {
                        puts "Backup password removed the firmware password successfully."
            -re "#" {
                puts "Password removed successfully with primary password."
    "Password Enabled: No" {
        puts "No firmware password set."
    default {
        puts "Unexpected response from firmwarepasswd"
        exit 1

unmanageFromJamf() {
    local uname="$1"
    local pwd="$2"
    local server="$3"
    local LOGGED_IN_USER=$(stat -f%Su /dev/console)

    # Get current Jamf server if server not provided
    [[ -z "$server" ]] && server=$(defaults read /Library/Preferences/com.jamfsoftware.jamf.plist jss_url)

    # Ensure the server URL ends with a /
    [[ "${server: -1}" != "/" ]] && server="${server}/"

    # Get unique identifier for machine
    local udid=$(system_profiler SPHardwareDataType | awk '/UUID/ { print $3; }')

    # Get computer ID from Jamf server
    local compId=$(curl -sku ${uname}:${pwd} ${server}JSSResource/computers/udid/${udid}/subset/general -H "Accept: application/xml" | xpath -e "//computer/general/id/text()")

    # Check for curl errors or empty compId
    if [[ $? -ne 0 ]] || [[ -z "$compId" ]]; then
        osascript -e 'display dialog "Error fetching Comp ID from Jamf server." buttons {"OK"} default button "OK"'
        exit 1

    # Send unmanage command to machine
    curl -X POST -sku ${uname}:${pwd} ${server}JSSResource/computercommands/command/UnmanageDevice/id/${compId}

function sendToGoogleSheet {
    # Gather system details
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    loggedinuser=$(stat -f%Su /dev/console)
    serialnumber=$(system_profiler SPHardwareDataType | awk '/Serial/ {print $4}')
    macosversion=$(sw_vers -productVersion)

    # Check for the presence of the jamf binary
    if which jamf > /dev/null 2>&1; then

    # This will base64-encode your data
    ENCODED_DATA=$(echo -n "$DATA_TO_SEND" | base64)

    # Ensure DATA_TO_SEND is not empty
    if [[ -z "$DATA_TO_SEND" ]]; then
        echo "Error: DATA_TO_SEND is empty."
        return 1

    # Ensure ENCODED_DATA is not empty
    if [[ -z "$ENCODED_DATA" ]]; then
        echo "Error: ENCODED_DATA is empty."
        return 1

    # URL of the deployed Google Apps Script web app

    # Use curl with the -L flag to follow redirects and send a GET request to the Google Apps Script web app
    curl -L "$WEB_APP_URL"

    echo "Data sent to Google Sheet."

# Variables from Jamf
LOGGED_IN_USER=$(stat -f%Su /dev/console)

unbindFromAD "$ADuser" "$ADpw"
deleteFirmwarePassword "$firmwarePassword1" "$firmwarePassword2"

# Promote to admin, if not an admin already
promoteToAdmin "$LOGGED_IN_USER"

# Create Backup Local Account
backup_password="localbackup"  # Modify as needed
sysadminctl -adminUser "$ADuser" -adminPassword "$ADpw" -addUser $backup_username -fullName "Local Backup User" -password $backup_password -admin
echo "Created backup admin user: '$backup_username'"

# Remove ecadmin account
if [ "$accountName" != "$LOGGED_IN_USER" ]; then
    /usr/sbin/sysadminctl -deleteUser "$accountName"
    echo "$accountName deleted"


# Send custom event to Jamf Pro
/usr/local/jamf/bin/jamf policy -event customSuccessEvent

# This should quit Self-Service.
killall "Self Service"

# Adjust volume
osascript -e "set volume output volume 80"

# Play sound and show dialog with a 15-second timeout
USER_RESPONSE=$(osascript <<EOD
do shell script "afplay /System/Library/Sounds/Ping.aiff"
delay 1
display dialog "Removing college settings..\nThis will only take a moment\n\nAll the best in the future!" buttons {"Play Beats", "See ya!"} default button "See ya!" with icon file (POSIX file "/Applications/Self") as alias giving up after 15
return button returned of result

# Check the response
if [[ $USER_RESPONSE == "Play Beats" ]]; then
    open -a "Google Chrome" ""

# Background the sleep and kill sequence
    sleep 10
    pkill -a -i 'osascript'
} &

# Determine the current computer name

# Append the abbreviation to the current name

echo "Computer name was: '${currentComputerName}' set to '${newComputerName}'"

# Update the computer name in Jamf Pro Server
/usr/local/jamf/bin/jamf setComputerName -name "$newComputerName"

# Run a recon to report back the updated inventory data to Jamf Pro
/usr/local/jamf/bin/jamf recon

unmanageFromJamf "$8" "$9" "${10}"
echo "Completed unmanageFromJamf"

# Run a recon to report back the updated inventory data to Jamf Pro
checkJamfBinary && /usr/local/jamf/bin/jamf recon

# Verify Jamf binary existence before sending the data to Google
if [ $? -eq 0 ]; then
    echo "Warning: Jamf binary still exists!"
    echo "Sending info to Google..."

sleep 15
# Lastly, remove the Jamf framework and perform any other necessary cleanup.
# Given that this is at the end, it ensures all previous tasks reliant on the Jamf binary have completed.
checkJamfBinary && {
    echo "Running the jamf removeFramework command..."
    /usr/local/jamf/bin/jamf removeFramework

# Exit the script cleanly
exit 0


Key Steps in the Script:

  1. Remove Policy Banner: Erases any existing Policy Banner from the device.
  2. Remove NoMAD Login: If NoMAD Login components are found, the script resets the login window to its default configuration.
  3. Unbind from Active Directory: Uses provided credentials to sever the connection between the device and Active Directory.
  4. Delete Firmware Password: Strips the firmware password from the machine using one of the two supplied passwords.
  5. Promote to Admin: Checks if the logged-in user is an admin and promotes them if they are not.
  6. Create Backup Local Account: Generates a new local user with administrative rights to serve as a backup.
  7. Remove FortiClient: Finds and uninstalls FortiClient from the Mac, if present.
  8. Unmanage Device from Jamf Pro: Executes an API call to mark the device as unmanaged in the Jamf Pro server.
  9. Send Data to Google Sheet: Encodes and dispatches device details to a specified Google Sheet, which acts as a log.
  10. Post-Processing and Cleanup: Includes computer renaming, updating inventory via Jamf recon, and the crucial step of eliminating the Jamf framework from the device.

I'm sharing a script that's been developed to off-board Mac devices remotely via Jamf Pro and am curious to hear if anyone in the community has tackled a similar challenge. The script effectively completes all off-boarding tasks, but the devices remain "managed" in Jamf Pro until the “Allow Jamf Pro to perform management tasks” checkbox is unchecked manually within the inventory tab. My goal is to automate this final step to fully optimize the off-boarding workflow. Does anyone have experience with this, or can anyone provide guidance on how to automate the unchecking of this box?

I appreciate any advice or shared experiences in making this process as efficient as possible.


Honored Contributor II

You got yourself a good-looking script, however Apples recommendation is to wipe a device when it changes hands.

What to do before you sell, give away, trade in, or recycle your Mac - Apple Support


I would suggest simply using the Wipe Computer MDM Command in JAMF. It takes about 30 seconds to hit the device, and the data is sanitized (crypto erased if FileVault is enabled). Apple Silicon Macs take about 5 minutes to set up after the command is received. Intel Macs need an OS reinstall, but it leaves you in recovery.



New Contributor III


Thanks AJ,

This script applies to students that wish to retain there data in place and it is a more flexible solution to offboard 200+ devices without wiping them, this logs the results in a google doc and the renaming means we have the historical records going forward without a licence.

We do in fact use your suggested method in another script to wipe spare devices, primarily due to onboarded staff and students throughout the year.

I greatly appreciate you taking the time to respond, always good to have feedback,