19 Aug MacBook Imaging at Scale: Restore, Verify, and Checkout
This post is about what happens when a Mac imaging workflow grows large enough that manual attention becomes the bottleneck. At the Marriott Library, a student checkout fleet that grew during COVID eventually forced us to rethink more than restore speed. We had to make software installation repeatable, keep course-specific apps current, handle remote identity constraints, close the SecureToken and Volume Ownership gap, and prove that a returned Mac was actually ready for the next borrower.
For MacAdmins, the interesting part is not a single magic tool. It is the handoff between tools: Jamf Pro for enrollment and policy orchestration, Installomator and other automation for application lifecycle work, DFU Blaster Pro and USB hubs for multi-device restore, XCreds for login-window identity and ownership setup, worker scripts for objective verification, and swiftDialog for a final pass/fail result that Help Desk staff can see without reading logs first.
The goal was to move reimaging from a specialist ritual to an operational service. We wanted the bench process to become: plug in the USB-C and network cables, wake the MacBook, walk away, and let the automation run. A Mac should not merely be erased and re-enrolled; it should come back with the right software, the right administrator ownership state, the right network assumptions, and a visible verification result. The rest of this post walks through how those requirements emerged and how the workflow became something a broader support team could run with confidence.
History and Background
CARES Act Expansion
During COVID, the J. Willard Marriott Library was asked to scale a student technology lending program far beyond its normal operating size. The library had already loaned laptops and other technology to students, but campus closure and remote instruction changed the size and urgency of that work almost overnight.
The turning point was CARES Act funding. In August 2020, the Marriott Library announced that $1.4 million in CARES funding had been allocated to expand student technology lending. The library procured 1,000 additional laptops for fall semester, including 650 PCs and 350 Macs, plus 100 mobile hotspots. Combined with existing and quickly gathered devices from across campus, the library expected to circulate more than 1,200 laptops and more than 200 Wi-Fi devices to students during fall semester.
The checkout cadence also changed the operational math. Student laptop checkout had been tied to semester-length borrowing periods, but the current model is 60 days, and many devices are returned sooner. That shorter and more variable cycle makes quick return-to-service more important. Every returned Mac needs to be erased, restored, enrolled, patched, checked, and made ready for the next borrower without turning each return into a custom MacAdmin project.
That growth was important and necessary, but it came with a very real operational cost. The University of Utah is a decentralized campus, and the requests we received reflected that reality. Some students needed a generic laptop for coursework. Others needed software for a specific department, a single course, a research workflow, or a personal accessibility or support need. The checkout pool had to be flexible and modular enough to serve a campus, not just a single standard image or suite of software and applications.
Remote Identity and the VPN Gap
At the same time, identity and remote support were more complicated than they looked from the outside. The campus still relied heavily on on-premises Active Directory, but we were not binding the Macs directly to AD. Instead, we used NoMAD as a looser directory integration layer: local macOS accounts stayed local, while NoMAD helped users authenticate against AD and keep their local password aligned with their university password. In normal times, a student would be on campus, on the campus network, or close enough to a support desk that authentication and password issues could be handled directly. During COVID, we were mailing laptops directly to students. That meant we had to stage temporary local access or use a university ID with a temporary password until the student could log in, connect to VPN, reach the restricted campus Active Directory environment, and synchronize or use their real university password.
At the time, remote access depended on the campus VPN service. During the COVID-era workflow, that meant Cisco AnyConnect. After that period, the campus VPN direction moved to GlobalProtect. The product name was not the main issue, though. The real problem was timing: a VPN connection after login did not help if the first login or password synchronization needed access to on-premises Active Directory. Even without a traditional AD bind, the NoMAD-era workflow still needed line-of-sight to AD at the right moment so the user could authenticate and align the local Mac account with the university account.
Unlike Windows, native macOS does not provide a general-purpose, built-in “start VPN before user login” workflow for arbitrary VPN configurations. Standard VPN profiles and most GUI-based VPN clients normally depend on an active user session: the user logs in, the client launches, the tunnel connects, and then protected campus resources become reachable. That order works for many staff workflows, but it is backwards when the first login itself depends on the network path.
There are enterprise exceptions and vendor-specific approaches, but they come with real design constraints. Cisco documents Start Before Login as a way to provide access to local infrastructure, domain logon scripts, Group Policy Objects, and other Active Directory functionality during logon. Palo Alto documents GlobalProtect pre-logon behavior for Windows and Mac endpoints, including machine-certificate-based pre-logon access and the transition to the actual user connection after login. Those are useful patterns, but they are not the same as a simple native macOS switch that makes any VPN available at the login window.
That distinction mattered for mailed student Macs. A VPN-at-login-window workflow needs the right client support, certificate or profile deployment, authentication model, FileVault behavior, captive portal handling, enrollment timing, and user-transition behavior. A setup that works for tightly managed staff devices may still be fragile for high-volume student checkout devices shipped to homes with temporary credentials. When devices were picked up in person, Help Desk staff could use their local knowledge and require the student to log in while still on campus, confirming that the campus login path was set up and working before the student left. Mailed devices did not have that same safety net.
During the COVID response, the changes and needs were also quick and sporadic. Requests changed as courses moved online, departments adjusted requirements, and students reported different home-network and access situations. There was not enough time to fully design, test, and support a macOS VPN-at-login-window architecture across the student checkout fleet. We had to implement simpler workarounds that fit the organization’s existing infrastructure and could be explained to students and support staff under emergency timelines.
The practical alternatives were less glamorous but more sustainable. macOS mobile accounts and cached credentials can let a previously synced directory user log in offline. Temporary local access can get the student to the desktop so VPN and enrollment tasks can be completed after login. NoMAD helped avoid the pain of traditional AD binding, but it was later archived and no longer actively maintained. That pushed the workflow toward XCreds, which better matched the direction we needed for modern login-window identity, local account synchronization, and the SecureToken/Volume Ownership automation described later in this post. For our workflow, the important lesson was not to assume that a post-login VPN solved a pre-login identity problem.
That gap shaped a lot of the early operational compromises. We were trying to support remote students quickly while still preserving a managed, supportable Mac environment.
Related reading:
- Marriott Library, “Marriott Library Allocated CARES Funding to Lend More Laptops for Students”: https://blog.lib.utah.edu/marriott-library-allocated-cares-funding-to-lend-more-laptops-for-students/
- Marriott Library, “Student Checkout Devices Presentation”: https://apple.lib.utah.edu/marriott-library-student-checkout-devices-presentation/
- Cisco, “Start Before Login VPN documentation”: https://www.cisco.com/c/en/us/td/docs/security/vpn_client/anyconnect/Cisco-Secure-Client-5/admin/guide/b-cisco-secure-client-admin-guide-5-0/configure_VPN.html
- Palo Alto Networks, “GlobalProtect Portals Agent App Tab”: https://origin-docs.paloaltonetworks.com/ngfw/help/12-2/globalprotect/network-globalprotect-portals/globalprotect-portals-agent-configuration-tab/globalprotect-portals-agent-app-tab
- Apple Support, “Set up mobile user accounts in Directory Utility on Mac”: https://support.apple.com/guide/directory-utility/diruc37c9dad/mac
- Jamf, “Jamf to Archive NoMAD Open-Source Projects”: https://www.jamf.com/blog/jamf-to-archive-nomad-open-source-projects/
The First Scaling Problem: Software
The first major pressure point was software. The student checkout fleet could not be treated like a static lab image. Students and faculty were making requests tied to courses, departments, and individual needs, and those requests changed over time.
Automating the Software Lifecycle
To reduce the manual work of packaging and updating applications, we moved more of the software lifecycle into Jamf Pro Patch Management and Title Editor. Later, we added Jamf App Catalog where it fit. For titles that were not supported by those tools, we leaned on the open source Installomator project.
Installomator
Installomator was useful because it gave us a repeatable pattern for applications that did not fit neatly into a vendor catalog or a maintained patch title. The traditional workflow was slow: download and expand the vendor installer, repackage it, upload the package to Jamf Pro, configure policies and scopes, deploy it, and then repeat the same cycle for the next version. In the Software Lifecycle Whisperer presentation, that packaging loop was called out as a real operational bottleneck, with new or updated software requests sometimes taking one to two days of package work before they were ready to deploy.
Installomator helped us treat many unsupported third-party apps as scripted installs instead of custom package projects. A label acts like a small recipe for an application: it defines the app name, installer type, download URL, expected Apple Developer Team ID, and other optional logic such as current-version discovery, blocking processes, or a vendor-provided command-line installer. That lets us move more app work toward repeatable download, verification, install, and update logic instead of rebuilding packages for every release.
At Marriott Library, that mattered because the software list was not static. The student checkout fleet had to absorb course-specific and department-requested apps, rapid update needs, and high-volume device turnover. We used Installomator where it fit, but still kept other approaches for cases that needed repackaging, licensing files, post-install content, or nonstandard commercial installers. The local pattern became a wrapper-driven Jamf workflow: validate likely installation paths, choose an update method such as Installomator, a Jamf installer update, or Adobe RUM, and then call a named update trigger such as [app_name]_update. That gave us a way to support scheduled weekly updates while still shortening the timeframe for urgent CVE-driven updates.
Installomator also gave the team better troubleshooting handles. Its debug modes could test download and verification logic before changing a client system, and its logs and exit codes made failed installs easier to diagnose from Jamf policy history or /var/log/Installomator.log. That was important for a checkout environment where a failed or stale app was not just an IT nuisance; it could become a student support problem during a class or assignment window.
python-jamf
We also developed our own Jamf Pro automation tooling to remove repetitive console work from package and patch maintenance. The python-jamf project provides a Python library for interacting with Jamf Pro objects. On Monday, March 2, 2026, the jctl and pkgctl command-line tools moved into the same project. Together, they gave us a way to treat Jamf Pro configuration as something we could query, update, compare, and repeat instead of clicking through the web interface one object at a time.
That mattered because the bottleneck was not only installing software on Macs. It was keeping the management system itself current as titles, packages, versions, policies, scopes, scripts, and patch definitions changed. A routine software update could touch several Jamf Pro objects: a package upload, a policy package reference, a policy version note, a smart group or scope adjustment, a patch-management definition, and sometimes a custom trigger or script parameter. Doing that manually across many titles invited drift.
With python-jamf, jctl, and pkgctl, those changes could become repeatable operations:
- Inspect Jamf Pro objects before changing them
- Update installer package references in policies
- Adjust policy names, triggers, scopes, or enabled state as the software moved through testing
- Automate bulk package and policy changes instead of editing each object by hand
- Help maintain patch-management workflows as software titles and versions change
- Make Jamf Pro updates easier to review, repeat, and document
That kind of automation was especially useful for student checkout because the device pool had to absorb changing course and department needs without turning every software request into a one-off console session.
That mix gave us a better software supply chain:
- Jamf Pro Patch Management and Title Editor helped track and update known software titles.
- Jamf App Catalog helped with supporting third-party application delivery.
- Installomator filled in gaps for applications that needed scripted download, verification, install, and update logic outside the supported catalog and patch-title paths.
python-jamf,jctl, andpkgctlhelped automate Jamf Pro package, policy, scope, script, trigger, and patch-management changes that otherwise required repeated manual actions in the web interface.
This was not just about making installs faster. It was about making the checkout fleet survivable. The more software work we could express as repeatable policy, patch, catalog, or script logic, the less the team had to solve the same problem by hand on one returned laptop at a time.
Related reading:
- Marriott Library Apple Infrastructure, “The Software Lifecycle Whisperer Presentation”: https://apple.lib.utah.edu/the-software-lifecycle-whisperer-presentation/
- Marriott Library Apple Infrastructure, “Installomator – Post-Install Policies, Triggers, and Follow-Up Actions”: https://apple.lib.utah.edu/installomator-post-install-policies-triggers-and-follow-up-actions/
- Marriott Library Apple Infrastructure, “From Manual to Automated-Managing Jamf Title Editor at Scale”: https://apple.lib.utah.edu/title-editor-api-automation-toolkit/
- University of Utah Marriott Library Apple,
python-jamfGitHub repository: https://github.com/univ-of-utah-marriott-library-apple/python-jamf - Jamf Pro Documentation, “Software Title Updates”: https://learn.jamf.com/r/en-US/jamf-pro-documentation/Software_Title_Updates
- Jamf, “Title Editor Documentation”: https://www.jamf.com/resources/product-documentation/title-editor-documentation/
- Jamf, “Jamf App Catalog”: https://www.jamf.com/resources/product-documentation/jamf-app-catalog/
- Installomator GitHub repository: https://github.com/Installomator/Installomator
The Second Scaling Problem: Reimaging
As MacBooks came back from student checkout, we needed a reliable way to erase, restore, re-enroll, reinstall baseline software, and validate that each device was ready for the next borrower.
Initially, we used DFU Blaster to help image returned MacBooks. That was a big step forward from fully manual recovery work, but the early workflow still had limits. It did not yet support the high-volume imaging process we wanted, and troubleshooting still depended heavily on IT staff watching the process, interpreting failures, and manually validating the final state.
That was workable when the volume was low. It was not a good long-term staffing model.
The next step came as DFU Blaster Pro added better support for multi-device workflows and Acroname USBHub3c hubs. Twocanoes describes DFU Blaster Pro as a tool for putting Apple silicon Macs into DFU mode and restoring supported macOS versions. Its Pro workflow includes one-to-many restore options, third-party USB hub support, Acroname USBHub3c integration, auto-DFU behavior, asynchronous restores, restore history, and clearer device status.
What the DFU Blaster Pro Bench Shows
The DFU Blaster Pro interface became important because it gave the bench a shared operational view. Instead of treating each connected MacBook as a separate manual restore, the main window shows the available devices, hub address, hub port, DFU or Recovery state, restore status, ECID, serial number when available, and USB address. That made it easier for Help Desk staff to see which devices were connected, which port a device belonged to, and which systems were ready for the next action.
The toolbar and automation controls also made the workflow easier to explain. Staff could use actions such as DFU, Restart, Recovery, Restore, History, Look up DFU Port, Log, and Settings from one place. For our bench automation, the two most important toggles were “Automatic DFU“ and “Restore Automatically“. Automatic DFU puts a Mac plugged into an Acroname hub or the admin Mac’s DFU port into DFU mode. Restore automatically starts the restore when a device is attached in DFU mode. Together, those options moved the workflow closer to the goal of connecting the MacBook, waking it, and letting the bench automation take over.
The screenshot shows the three windows we normally kept visible while testing the bench. “DFU Blaster Pro“ was the primary operator view: it showed the connected Macs, hub ports, DFU or Recovery state, restore controls, and automation toggles. “Apple Configurator“ was visible as a second source of truth. It does not have to be open for the workflow to run because DFU Blaster Pro uses Apple Configurator’s restore tooling behind the scenes, but keeping it open during testing helped us confirm when each Mac entered DFU mode and compare Apple Configurator’s device view against DFU Blaster Pro’s status table. The “terminal “was open for support tasks around the bench. We used Mist to download current IPSW files for the macOS versions we supported, including macOS 26.6.2 during this phase of testing, and kept Terminal available for miscellaneous commands, log review, and debugging when a restore, USB connection, or enrollment step did not behave as expected.
The settings view mattered because some of the most useful scaling behavior lives there. DFU Blaster Pro can use an event script, keep the DFU Blaster window in front, configure Acroname-specific automation behavior, and show license information. For a bench workflow, those settings are not decorative. They are part of turning a one-off restore tool into a repeatable station where the same actions happen in the same order and where restore events can be logged or exported for later troubleshooting.
Building a Multi-Device Imaging Bench
With Acroname hubs, the direction was clear: make the imaging bench a repeatable station where multiple returned MacBooks could be connected, restored, and moved through the same onboarding flow. We purchased additional Acroname hardware with the goal of imaging and onboarding 10 MacBooks at a time.
That goal had a throughput tradeoff. A single Mac could be restored in about 10 minutes, while a 10-system batch took roughly 35 to 40 minutes. The larger batch was still a better use of staff time, but it was not a pure 10-times-faster multiplier. In the DFU at scale discussion, Twocanoes described newer USB 3 restore behavior that can make individual restores much faster on supported newer Macs, with the important caveat that the fastest path depends on hardware generation, cabling, hub topology, and USB behavior. In our environment, the practical question was not the best possible benchmark. It was whether a batch workflow could be predictable enough to fit the checkout return cycle.
As the bench moved toward larger batches, the workflow had to balance total devices processed against restore time, USB complexity, and troubleshooting risk. We could add another hub and push the bench toward 15 systems at the same time, but the question became less about maximum capacity and more about value: would the added throughput actually benefit our maintenance windows and return schedule, or would it add complexity without improving the service?
That goal was not immediately achieved. Apple Configurator behavior, DFU Blaster behavior, USB topology, cables, hardware timing, and ordinary environmental stability all mattered. The DFU port itself varies by model, and at scale, that small detail becomes a real source of failed attempts. Cable quality matters too. A cable that is fine for one manual restore may not be the cable you want in a bench that is restoring many Macs every day. The same was true of power. Some returned devices have little or no battery charge, and a restore workflow can fail before it really starts if the bench cannot provide enough power and data reliably.
For that part of the bench, we added a Satechi 200W USB-C 6-Port GaN Charger so low-battery returns could get a baseline charge before imaging. Satechi describes the charger as a six-port USB-C PD charger with up to 200W of total output and automatic power distribution across connected devices. The practical target was not to fully charge every returned MacBook before restore. It was to get the device above the failure-prone empty-battery state. The DFU Blaster Pro developer’s guidance was to make sure target Macs have at least about 5-10% battery before starting the workflow.
The other lesson was that the restoration is only one part of the work. Returned laptops still have to be received, cleaned, asset-tagged, identified, connected, restored, enrolled, and checked. If the serial number is hard to read, if a port is dirty, if the device is depleted, or if a cable behaves inconsistently under load, the restore timer is not the whole operational timer. At the checkout scale, staff time disappears into all of those edges.
When we tried to push toward 10-at-a-time imaging, failures still required IT troubleshooting and manual intervention. The promise was there, but the process was not yet ready to move outside the MacAdmin team.
Over time, DFU Blaster Pro became more stable and more automation-oriented. Twocanoes’ version history shows the direction of travel: Acroname improvements, auto-retry behavior, asynchronous restore operations, integrated hub functions, automatic DFU and restore toggles, better history, and more descriptive statuses. That matters because failures do not disappear at scale. The better goal is to make failures recoverable, visible, and easy for a technician to interpret without camping in front of every Mac.
The transcript also reinforced why DFU restore still has a role even in an MDM-first world. An MDM erase command can return a Mac to the OS already on the device, but a DFU restore can put the Mac onto a known supported macOS version before Automated Device Enrollment begins. That is valuable when devices were purchased months earlier, returned from borrowers in unknown states, or need to meet a current support baseline before being handed to the next student. Once the restored Mac reaches Setup Assistant, Ethernet and MDM enrollment can take over and continue the provisioning flow.
Those improvements changed the question from “Can IT make this work?” to “Can we make this safe and clear enough for Help Desk staff to run?”
That was the real milestone. The goal was not just faster imaging. The goal was moving a repeatable process to the right support tier so the IT team could spend more time on higher-scope projects.
Related reading:
- Twocanoes, “DFU Blaster Pro”: https://twocanoes.com/products/mac/dfu-blaster/
- Twocanoes, “DFU Blaster Pro Admin Guide”: https://twocanoes.com/knowledge-base/dfu-blaster-pro-admin-guide/
- Twocanoes, “DFU Blaster Pro Version History”: https://twocanoes.com/products/mac/dfu-blaster/history/
- Twocanoes, “Restoring Apple Silicon Devices with DFU Blaster Pro and the Acroname USBHub3c”: https://twocanoes.com/knowledge-base/restoring-apple-silicon-devices/
- Twocanoes, “DFU at Scale” presentation: https://www.youtube.com/watch?v=–RGlACJD14
The Third Scaling Problem: Trusting the Result
Restored Does Not Mean Ready
A restored Mac is not automatically a ready Mac.
After the DFU restore and Jamf Pro enrollment, we still needed to know that the Mac had reached a usable state:
- Was the expected local admin account present?
- Did that account have admin membership?
- Did it have SecureToken?
- Was it a Volume Owner?
- Did the expected default applications install?
- Was the Mac on the expected network?
- Did the final login-window notification clearly tell staff whether the process passed or failed?
Those details matter because checkout Macs have to support future software updates, administrative workflows, and student use without a MacAdmin standing next to the device. A process that looks successful but leaves the admin account without SecureToken or Volume Ownership will create support debt later.
Apple’s deployment guidance describes the workflow behind that dependency. On APFS, SecureToken is tied to the user’s password-protected encryption key material. In a managed deployment, the first secure-token-enabled login can generate and escrow a bootstrap token to the device management service. On Apple silicon Macs, that bootstrap token can then help authorize software updates, Erase All Content and Settings, and the granting of SecureToken to additional users. Apple also notes that when a bootstrap token is available, it can grant Volume Ownership as it grants SecureToken, which is why the validation step had to check both states instead of assuming that admin membership alone was enough.
This was especially important because our local admin account is used by the open source S.U.P.E.R.M.A.N. workflow to manage macOS updates across student checkout laptops, labs, and staff Macs. If that local admin account does not have the right token and ownership state, later macOS update workflows can fail or require hands-on remediation.
Related reading:
- Apple Platform Deployment, “Use secure token, bootstrap token, and volume ownership in deployments”: https://support.apple.com/guide/deployment/use-secure-and-bootstrap-tokens-dep24dbdcf9e/web
- Jamf Pro Documentation, “Provisioning Local Accounts During Automated Device Enrollment”: https://learn.jamf.com/r/en-US/jamf-pro-documentation-current/Configuring_Local_Account_Provisioning
- Macjutsu, “S.U.P.E.R.M.A.N.” GitHub repository: https://github.com/Macjutsu/super
The SecureToken and Volume Ownership Gap
One of the hardest parts of the workflow was local admin SecureToken and Volume Ownership.
Jamf Pro PreStage Enrollment can create a local administrator account, but in our testing that account did not consistently receive SecureToken or Volume Ownership. Historically, one workaround was to log in interactively as the local admin account so macOS would grant the needed state. That was exactly the kind of manual step we were trying to remove from the returned-device workflow.
That manual login existed for a reason. Apple’s deployment documentation ties SecureToken and bootstrap-token behavior to account creation, password setting, and user login events. Jamf’s documentation says managed administrators can receive a SecureToken during login if a bootstrap token is escrowed to Jamf Pro, and its bootstrap-token guidance similarly describes escrow happening when a secure-token-enabled user signs in at the macOS login window. In practical terms, simply creating the local admin account during enrollment did not prove the account had crossed the cryptographic threshold needed for future management. Logging in as that account was the blunt but reliable way to make macOS evaluate the account, grant the needed SecureToken path when conditions were right, and then allow Volume Ownership to follow on Apple silicon Macs.
XCreds Provided the Missing Login-Window Context
We worked with the developer of XCreds and DFU Blaster to test a better solution. The XCreds command-line interface can store a local admin user for password-reset and secure-token workflows:
/Applications/XCreds.app/Contents/MacOS/XCreds set-admin-user \
--adminusername [LOCAL_ADMIN_NAME] \
--adminpassword [LOCAL_ADMIN_PASS]
Looking at the XCreds documentation and source helps explain why this works. The set-admin-user command requires root and stores the supplied local admin username and password as XCreds local admin credentials. Twocanoes documents that XCreds 5.2 and later stores these local secrets in an encrypted file at /usr/local/var/twocanoes/secrets.bin, protected by root-only file permissions and an asymmetric key in the system keychain named XCreds Encryptor. The local admin password is also associated with the Mac’s serial number, and access is limited to XCreds and the login-window process. During the login-window flow, the XCreds user-setup mechanism reads those saved credentials back from that protected local secrets store and passes them forward as the localAdmin authorization hint. Other XCreds mechanisms can then use that hint when a password overwrite, keychain reset, FileVault authorization, or secure-token-related operation needs a real local admin credential.
That storage model is important to describe accurately. The command does not magically grant SecureToken by itself, and it does not remove the need to protect the Mac. It makes the admin credential available to XCreds at the exact point macOS cares about: the login-window authentication workflow. Twocanoes recommends protecting /usr/local/var/twocanoes/ so only the system and root can read it, using full disk encryption where possible, and using firmware or recovery-access protections where applicable so the system keychain and its password cannot be taken from the device offline.
The important behavior came later at the login window. XCreds 5.9 added secure-token behavior for MDM-created admin users: if admin credentials have been entered with the XCreds command-line utility, the admin user lacks a secure token, and a bootstrap token is available, XCreds can perform a silent interactive login flow far enough to obtain the secure token, without completing the login to the desktop. Twocanoes documents that this behavior can be skipped with the shouldSkipSettingSecureTokenForAdmin preference.
In testing, that solved more than SecureToken. Because macOS bootstrap-token behavior can also grant volume ownership as it grants secure tokens, the local admin account ended up with both SecureToken and Volume Ownership after the login-window activation. In the MacAdmins Slack discussion, that was the “twofer”: the admin was never logged in interactively at the desktop, but after XCreds ran at the login window, the account had both states needed for downstream management.
That changed the imaging workflow. We no longer needed a person to log in as the admin account just to make the Mac administratively usable.
Jamf Pro Script Setup
In Jamf Pro, we used a small helper script to set the desired local administrator account for the XCreds secure-token workflow. The script takes the local administrator username and password from Jamf Pro script parameters, then runs the XCreds set-admin-user command so XCreds can use that credential during the login-window flow.
The script is intended to be uploaded to the Jamf Pro server as “XCreds SecureToken and Volume Owner Setup”. At the Marriott Library, we used it after PreStage enrollment in a Jamf Pro policy triggered by “Enrollment Complete“. In the policy, Parameter 4 is the local administrator username, and Parameter 5 is the local administrator password. Those values should come from the organization’s managed local administrator credential workflow, not be hard-coded into the script.
Using a Jamf Pro script made the workflow more repeatable than typing the XCreds command by hand or relying on a technician to remember the exact timing. It also kept the script reusable across environments because the local administrator credential was supplied at policy runtime instead of being embedded in the script itself. If the target local administrator account needed to be changed later, we could update the Jamf Pro policy parameters instead of editing and redistributing the script. Most importantly, it moved the SecureToken and Volume Ownership setup into the automated enrollment path, before the Mac reached the Help Desk bench as a device that looked finished but still needed a manual admin login to become manageable.
A sanitized copy of the script will be posted to GitHub here: xcreds_securetoken_and_volume_owner_setup.sh.
Related reading:
- Apple Platform Deployment, “Use secure token, bootstrap token, and volume ownership in deployments”: https://support.apple.com/guide/deployment/use-secure-and-bootstrap-tokens-dep24dbdcf9e/web
- Jamf Pro Documentation, “Provisioning Local Accounts During Automated Device Enrollment”: https://learn.jamf.com/r/en-US/jamf-pro-documentation-current/Configuring_Local_Account_Provisioning
- Jamf, “Manually Creating and Escrowing the Bootstrap Token”: https://learn.jamf.com/r/en-US/technical-articles/Manually_Creating_and_Escrowing_the_Bootstrap_Token
- Twocanoes, “XCreds Command Line Interface Guide”: https://twocanoes.com/knowledge-base/xcreds-command-line-interface-guide/
- Twocanoes, “XCreds Local Secrets Cryptography”: https://twocanoes.com/knowledge-base/xcreds-local-secrets-cryptography/
- Twocanoes, “What’s New in XCreds 5.9”: https://twocanoes.com/knowledge-base/whats-new-in-xcreds-5-9/
Making Validation Visible
Once the underlying pieces were working, the next problem was human-facing: how does Help Desk staff know the Mac is done?
We built a login-window verification workflow around smaller worker scripts. Those scripts check things like:
- local admin SecureToken and Volume Ownership
- default application installation
- Wi-Fi state
Displaying swiftDialog at the Login Window
Getting the final notification to appear over the macOS login window has one important requirement: swiftDialog must be launched from the login-window context, not from a normal root script running in the background. The swiftDialog login-window documentation describes the required pieces as a script that launches dialog with --loginwindow and a LaunchAgent limited to the LoginWindow session. Outset provides another path to the same destination by running root-owned executable scripts from /usr/local/outset/login-window/ when the login window appears.
In this workflow, verify_reimage_loginwindow_notification.sh handles the staging work. The normal Jamf policy should run the script with --install, not --display. The install action runs the verification checks, writes the final result and dialog text to temporary files, creates an active flag, verifies that swiftDialog is available, and then stages a helper that will actually launch the dialog with --loginwindow.
The requirements are:
- swiftDialog must be installed, with
/usr/local/bin/dialogpreferred. - The Mac must reach the macOS login window. This is not the FileVault preboot authentication screen.
- The helper that calls swiftDialog must run in the login-window session.
- If using Outset, Outset must be installed and its LoginWindow LaunchAgent must be present.
- Outset login-window helper scripts must be owned by
rootand executable with mode0755. - If using the manual LaunchAgent path, the LaunchAgent must be installed in
/Library/LaunchAgents/, limited to theLoginWindowsession, and pointed at the helper script. - The workflow needs a cleanup condition so the dialog does not appear forever at every login window.
The script supports three launcher choices through Parameter 9 or --launcher: auto, outset, and launchagent. The default auto mode prefers Outset when it is useful, especially in the XCreds login-window state where /dev/console may show root while scutil State:/Users/ConsoleUser is still empty. If Outset is unavailable, the script falls back to creating its own LoginWindow LaunchAgent.
With the Outset path, the script writes this helper:
/usr/local/outset/login-window/000-reimageverificationdialog.sh
Outset then launches that helper when the login window appears. The helper reads the staged result files, calls swiftDialog with --loginwindow, shows the pass/fail notification, and removes this workflow’s helper and temporary files when the button is clicked. It does not remove Outset itself or Outset’s shared LoginWindow LaunchAgent.
With the manual LaunchAgent path, the script creates its own LaunchAgent:
/Library/LaunchAgents/edu.utah.lib.reimageverification.plist
That LaunchAgent is limited to the LoginWindow session and runs the staged helper from:
/Library/Scripts/ReimageVerification/reimageverificationdialog.sh
In practice, the deployment order is:
- Install swiftDialog.
- Install Outset if that will be the preferred login-window launcher.
- Run the worker verification policies so they write their receipts.
- Run the final notification policy with
verify_reimage_loginwindow_notification.sh --install. - Let the Mac arrive at the login window, or log out once after enrollment if needed.
- Confirm the verification dialog appears over the login window.
- Click Remove Verification after recording the result; the helper cleans up this workflow’s active files.
Customizing the Login-Window Notification
The final notification script, verify_reimage_loginwindow_notification.sh, is meant to be adapted before another organization uses it. A sanitized copy will be posted to GitHub here: verify_reimage_loginwindow_notification.sh.
The first values to customize are the organization name, dialog title, and banner colors:
ORGANIZATION_NAME="${REIMAGE_VERIFICATION_ORGANIZATION_NAME:-⌂ Marriott Library}"
VERIFICATION_DIALOG_TITLE="${ORGANIZATION_NAME} - Imaging Verification Workflow"
SUCCESS_BANNER="colour=#008000,nogradient"
FAILED_BANNER="colour=#cc0000,nogradient"
ORGANIZATION_NAME is the visible organization or service name in the final login-window dialog. It can be hard-coded in the script or supplied at runtime with the REIMAGE_VERIFICATION_ORGANIZATION_NAME environment variable or Jamf Parameter 8. VERIFICATION_DIALOG_TITLE builds the full dialog title from that organization name. SUCCESS_BANNER and FAILED_BANNER control the swiftDialog banner color for the final result. Another organization could change those colors to match its own support or campus branding, but the colors should stay obvious enough that a technician can recognize pass or fail from across the bench.
The next group defines the worker scripts and how the final notification describes them:
VERIFIER_SCRIPT_NAMES=(
"verify_local_admin_securetoken_volumeownership.sh"
"verify_default_apps_local_installed.sh"
"verify_uconnect_wifi_connected.sh"
)
VERIFICATION_DISPLAY_NAMES=(
"SecureToken and Volume Ownership"
"Default Applications"
"Uconnect Wi-Fi"
)
VERIFIER_SCRIPT_NAMES lists the worker script filenames that belong to the workflow. This matters most when the final notification script runs checks from local scripts or when it installs and cleans up workflow files. If another organization writes its own checks, this array should contain its script names, such as verify_battery_health.sh, verify_vpn_configuration.sh, or verify_required_certificates.sh.
VERIFICATION_DISPLAY_NAMES contains the human-readable names shown in logs and failure summaries. These should be written for the technician reading the result, not for the script author. For example, VPN Configuration is better than vpn_check, and Battery Health is better than battery_status_worker.
The next array is used when the notification script runs checks directly with --source local-scripts:
verification_items=(
"securetoken_volumeownership"
"default_apps"
"uconnect_wifi"
)
verification_items contains internal identifiers that map to the run_verification_item() case statement inside verify_reimage_loginwindow_notification.sh. If an organization uses local-script mode, it must add a new identifier to this array and add a matching case item that calls the correct worker script. For example, a battery_health item would need a corresponding battery_health) case in run_verification_item().
The final array is used by the preferred receipt-based workflow:
receipt_items=(
"verify_local_admin_securetoken_volumeownership.status"
"verify_default_apps_local_installed.status"
"verify_uconnect_wifi_connected.status"
)
receipt_items lists the receipt files expected in /var/tmp/reimage_verification_receipts. Each worker policy writes one of those files with successful or failed as the first line. The final notification script reads those receipts, pairs them with VERIFICATION_DISPLAY_NAMES, and decides whether the overall workflow passed.
The display-name and receipt arrays should be kept in the same order. For example, the first display name should describe the first receipt file, and the first receipt file should be written by the first verification workflow. In our setup, the SecureToken and Volume Ownership check, default application check, and Wi-Fi check each had a matching script, display name, internal item name, and receipt file.
For a Jamf Pro deployment, the cleaner pattern is to create separate verification policies for each check. Each policy runs its own worker script and writes a status receipt such as successful or failed into /var/tmp/reimage_verification_receipts. Then a final policy runs verify_reimage_loginwindow_notification.sh with the default receipts source. That final policy reads the receipt files, builds the success or failure message, and stages the swiftDialog login-window notification.
The worker script can be organization-specific. It does not need to know anything about swiftDialog or the login-window notification. Its job is only to test one requirement and write one receipt file. For example, an organization that needs to verify its VPN client could use a worker script shaped like this:
#!/usr/bin/env bash
RECEIPT_DIR="/var/tmp/reimage_verification_receipts"
RECEIPT_FILE="${RECEIPT_DIR}/verify_vpn_configuration.status"
/bin/mkdir -p "$RECEIPT_DIR"
if [[ -d "/Applications/GlobalProtect.app" ]]; then
/bin/echo "successful" > "$RECEIPT_FILE"
exit 0
else
/bin/echo "failed" > "$RECEIPT_FILE"
exit 1
fi
The final notification script treats the first line of each receipt as the result. The value successful means that the check passed. A missing receipt, failed, or any other value is treated as a failed check. To add the VPN example to the final notification, the organization would add a matching display name and receipt filename:
VERIFICATION_DISPLAY_NAMES=(
"SecureToken and Volume Ownership"
"Default Applications"
"VPN Configuration"
)
receipt_items=(
"verify_local_admin_securetoken_volumeownership.status"
"verify_default_apps_local_installed.status"
"verify_vpn_configuration.status"
)
Another organization can use the same shape with its own checks:
Those checks do not have to be limited to software. A site could add worker scripts for hardware or readiness diagnostics, such as battery health, battery cycle count, available storage, expected RAM, Apple silicon model support, failed disk SMART status where available, Wi-Fi or Ethernet state, VPN profile presence, MDM profile health, FileVault state, bootstrap token escrow, required certificates, printer setup, or department-specific application licensing. The important part is that each check answers one operational question clearly enough to write successful or failed.
- Create one worker script per thing that must be validated.
- Create one Jamf Pro policy per worker script.
- Give each policy a clear custom trigger name, such as
verify_securetoken_volumeowner,verify_default_apps, orverify_wifi. - Make each worker script write a matching
.statusreceipt into/var/tmp/reimage_verification_receipts. - Update
VERIFICATION_DISPLAY_NAMESandreceipt_itemsso the final notification knows what to report. - Run the final notification policy after the worker policies complete.
The script can also run Jamf policies directly by trigger name or policy ID. With the jamf-triggers source, Parameter 6 contains a comma-separated list of custom trigger names. With the jamf-policy-ids source, Parameter 7 contains a comma-separated list of policy IDs. That can be useful for testing or smaller workflows, but receipts are easier to troubleshoot at the bench because each worker policy leaves behind a clear status file that the final dialog can summarize.
The final script stages a swiftDialog result at the login window. It shows a simple pass/fail result:
Marriott Library - Imaging Verification Workflow Successful
or:
Marriott Library - Imaging Verification Workflow
Failed
For troubleshooting, see log:
/var/log/reimage_verification_notification_error.log
The failure state matters as much as the success state. A failed Mac should not quietly return to circulation. It should give the person standing at the bench a clear handoff: this device needs troubleshooting, and this is the log to inspect.
That user-facing detail is small, but it is part of what makes the workflow transferable. If the only people who can interpret a failed image are the people who wrote the scripts, the process has not really been delegated.
Related reading:
- swiftDialog Documentation, “Login Window”: https://swiftdialog.app/operation/login-window/
- swiftDialog GitHub Wiki: https://github.com/swiftDialog/swiftDialog/wiki
- MacAdmins Outset GitHub Wiki: https://github.com/macadmins/outset/wiki
- MacAdmins Outset Wiki, “Security”: https://github.com/macadmins/outset/wiki/Security
From MacAdmin Craft to Help Desk Workflow
The journey started as emergency scaling: get devices into students’ hands during COVID. Then it became software lifecycle work: keep the checkout pool useful across many different student and course needs. Then it became imaging work: erase and restore returned MacBooks efficiently. Finally, it became workflow design: make the process predictable, validated, visible, and safe enough for the Help Desk team.
The final shape is not one tool. It is a layered system:
- CARES-funded devices expanded the service footprint.
- Jamf Pro, Patch Management, Title Editor, App Catalog, and Installomator reduced software maintenance drag.
- DFU Blaster Pro and Acroname hubs made multi-device restore realistic.
- NoMAD supported loose AD integration without binding Macs directly to Active Directory.
- XCreds helped close the SecureToken and Volume Ownership automation gap.
- Worker verification scripts created objective pass/fail checks.
- swiftDialog made the final state visible at the login window.
The lesson is that imaging automation is not only about erasing devices faster. The real work is removing the hidden manual decisions around the edges: which software is installed, which account can administer the Mac, whether updates will work later, whether network state is correct, and whether a non-MacAdmin can tell when the process is complete.
When those edges are automated and visible, imaging becomes less of a specialist ritual and more of an operational service.









No Comments