
Short answer: add the technician to the admin group with dseditgroup, then write a one-shot launchd daemon that calls a de-escalation script at a fixed wall-clock time. The scripts below do both and the daemon unloads itself when it fires. On Figment, this is most natural on a physical plan (i3 at $49/month, M-series from $95/month, provisioned on request) where you fully control the admin account; the $29 virtual Intel macOS plan also runs dseditgroup and launchctl in its browser-console desktop terminal, so confirm the admin account and user-management permissions you need in the quote before you rely on this in production.
A support technician or external consultant usually needs real privilege — install a kext, clear a signing cache, repair a broken Xcode install — and the tempting path is to promote their account to admin and forget to demote it. That dormant admin account then sits on a machine that may also hold CI secrets, signing material, and customer credentials. Time-boxing the privilege to a fixed window closes that exposure with one script pair and about five minutes of setup.
macOS uses three account types, documented in Apple's user accounts guide: an administrator can add and manage other users, install apps and change system settings; a standard user can install apps and change only their own settings; a sharing-only user can reach shared files remotely but cannot log in interactively.
Leaving an admin account in place after a session creates two problems. The credentials persist on a machine you share with build pipelines and keystores. And the technician keeps the ability to modify CI configuration, Keychain items or network settings well after the ticket closes. Pin the privilege to a window instead and both exposures end with the clock.
| Option | When to use | Revert step |
|---|---|---|
| Elevate an existing standard account | Technician already has a login you manage | Remove admin group membership; account remains standard |
| Create a dedicated temporary account | No pre-existing account; cleaner audit boundary | Delete the entire account at the end of the window |
The scripts below cover the elevation case. To use a fresh account, prepend sudo sysadminctl -addUser techsupport -password '<temp>' -fullName "Support Technician" to escalate.sh and append sudo sysadminctl -deleteUser techsupport inside deescalate.sh.
macOS groups live in Directory Services. dseditgroup is the documented command-line tool to edit them:
# Add techsupport to the admin group
sudo dseditgroup -o edit -a techsupport -t user admin
# Remove techsupport from the admin group
sudo dseditgroup -o edit -d techsupport -t user admin
# Check membership (exit 0 = member, exit 64 = not a member)
dseditgroup -o checkmember -m techsupport admin
Scripted confirmation uses the exit code from checkmember rather than parsing stdout.
Save both to /usr/local/bin/ owned by root with chmod 700 so an unprivileged user cannot modify them.
/usr/local/bin/escalate.sh
#!/bin/bash
# escalate.sh — add a user to admin and register a timed de-escalation
# Usage: sudo escalate.sh <username> <minutes>
# Example: sudo escalate.sh techsupport 120
set -euo pipefail
TECH_USER="${1:?Usage: escalate.sh <username> <minutes>}"
MINUTES="${2:?Usage: escalate.sh <username> <minutes>}"
AUDIT_LOG="/var/log/admin_escalation.log"
PLIST_LABEL="com.company.deescalate.${TECH_USER}"
PLIST_PATH="/Library/LaunchDaemons/${PLIST_LABEL}.plist"
# Confirm the user exists before touching groups
if ! id "${TECH_USER}" &>/dev/null; then
echo "ERROR: user '${TECH_USER}' does not exist." >&2
exit 1
fi
# Compute the de-escalation wall-clock time
DEESC_EPOCH=$(( $(date +%s) + MINUTES * 60 ))
DEESC_HOUR=$(date -r "${DEESC_EPOCH}" '+%-H')
DEESC_MIN=$(date -r "${DEESC_EPOCH}" '+%-M')
dseditgroup -o edit -a "${TECH_USER}" -t user admin
printf '%s ESCALATE user=%s until=%s\n' \
"$(date '+%Y-%m-%dT%H:%M:%S%z')" \
"${TECH_USER}" \
"$(date -r "${DEESC_EPOCH}" '+%Y-%m-%dT%H:%M:%S%z')" \
>> "${AUDIT_LOG}"
cat > "${PLIST_PATH}" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${PLIST_LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/deescalate.sh</string>
<string>${TECH_USER}</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>${DEESC_HOUR}</integer>
<key>Minute</key>
<integer>${DEESC_MIN}</integer>
</dict>
<key>RunAtLoad</key>
<false/>
</dict>
</plist>
PLIST
chown root:wheel "${PLIST_PATH}"
chmod 644 "${PLIST_PATH}"
# On macOS 13 (Ventura) and later, prefer: launchctl bootstrap system "${PLIST_PATH}"
launchctl load "${PLIST_PATH}"
echo "Escalation active. Auto-revocation scheduled at $(date -r "${DEESC_EPOCH}")."
echo "Plist: ${PLIST_PATH}"
/usr/local/bin/deescalate.sh
#!/bin/bash
# deescalate.sh — remove admin, clean up the plist, write an audit line
# Usually called by launchd; can also be run manually: sudo deescalate.sh <user>
set -euo pipefail
TECH_USER="${1:?Usage: deescalate.sh <username>}"
AUDIT_LOG="/var/log/admin_escalation.log"
PLIST_LABEL="com.company.deescalate.${TECH_USER}"
PLIST_PATH="/Library/LaunchDaemons/${PLIST_LABEL}.plist"
dseditgroup -o edit -d "${TECH_USER}" -t user admin 2>/dev/null || true
if dseditgroup -o checkmember -m "${TECH_USER}" admin &>/dev/null; then
printf '%s DEESCALATE_FAILED user=%s\n' \
"$(date '+%Y-%m-%dT%H:%M:%S%z')" "${TECH_USER}" >> "${AUDIT_LOG}"
exit 1
fi
printf '%s DEESCALATED user=%s\n' \
"$(date '+%Y-%m-%dT%H:%M:%S%z')" "${TECH_USER}" >> "${AUDIT_LOG}"
if [ -f "${PLIST_PATH}" ]; then
launchctl unload "${PLIST_PATH}" 2>/dev/null || true
rm -f "${PLIST_PATH}"
fi
echo "Admin access removed for ${TECH_USER}."
These are example scripts you should run through your own review and test on a non-production account before using in support rotations.
The plist uses a StartCalendarInterval dict with only Hour and Minute. Per Apple's launchd job guide, any missing calendar key behaves as a wildcard (the cron model), so a plist with just Hour and Minute fires at that time every day. That is fine here because deescalate.sh deletes the plist on the first run, so there is no second-day execution.
Two limitations to keep in mind. launchd only fires when the system is awake: if the Mac is asleep or powered off at the scheduled time, the job runs at the next wake past the trigger. Rental Macs usually stay on, but confirm your power settings before you depend on this. And the window is an upper bound on exposure, not a promise about what happened inside it — keep an audit log and backups regardless.
Both scripts append single-line records to /var/log/admin_escalation.log. After a session, confirm the lifecycle:
sudo grep "techsupport" /var/log/admin_escalation.log
# Example output:
# 2026-11-10T14:00:00+0000 ESCALATE user=techsupport until=2026-11-10T16:00:00+0000
# 2026-11-10T16:00:03+0000 DEESCALATED user=techsupport
The gap between ESCALATE and DEESCALATED is the real exposure window. /var/log/ is root-writable only, so a standard user cannot tamper with the record. Rotate the file with newsyslog if the machine stays up for months.
Append this before the final echo in deescalate.sh:
if pgrep -q -u "${TECH_USER}" Finder 2>/dev/null; then
sudo -u "${TECH_USER}" osascript -e \
"display notification \"Your admin access has been removed.\" with title \"Session Expired\""
fi
It only fires if the technician is still logged in, and on a VNC session the banner shows in the remote display.
If your support rotation is occasional and the technician mostly needs to inspect logs or re-run a build, the $29 virtual Intel macOS plan runs dseditgroup, launchctl and the audit log through its browser-console desktop terminal — inbound SSH is not currently offered, so operate the escalation from the console rather than a shell tunnel. If the technician needs to drive Xcode, sign for a physical Mac target, or test ARM-only binaries, choose the physical i3 at $49/month or M-series from $95/month and ask for the exact admin and remote-access configuration in the quote; physical hardware specs are confirmed when Figment confirms supply. Managed Build Mac at $249/month adds one M-series Mac with CI setup, backups, monitoring and 30 minutes of monthly support if you want the support window and the CI machine under one plan.
Start a configuration request at /mac-hosting/start, or compare plans at /mac-hosting.
Related reading: