
Recommendation: between each macOS test run, call tccutil reset <Service> <bundleID> for exactly the privacy services your test bundle uses, then verify by observing the next launch — the system permission alert must appear again. Do not read TCC.db directly as proof; the schema, service list and file location are private to Apple and have changed between releases. Scope every reset to one disposable test bundle ID under the current user. On Figment that work runs on the virtual Intel macOS plan at $29/month for user-permission flows, or on an M-series Mac from $95/month when you need ARM execution, Xcode 27 or supervised physical hardware (exact chip, RAM, storage and administrator access confirmed in the quote).
If your test suite depends on a first-run permission dialog — camera, microphone, contacts, calendar, reminders, photos — a leftover grant from an earlier run skips the alert and quietly changes which branch of the test actually executes. The fix is a small, supported pre-step: targeted tccutil reset calls for one bundle ID, followed by a visible fresh-prompt check for each service you care about.
tccutil reset Actually Does, and What It Doesn'tApple's Transparency, Consent and Control (TCC) subsystem records user privacy decisions. The one supported command-line surface for clearing those decisions is tccutil. Its local man page documents a single verb — reset — with the form tccutil reset <service> [<bundle-id>]. Read it on your own machine before trusting any blog table of service keys:
man tccutil
What tccutil reset <Service> <bundleID> does reliably on a current, SIP-enabled macOS:
sudo for user-scope services the current user already owns.What it does not do, and should not be claimed to do:
Camera, Microphone, ScreenCapture, Accessibility, AddressBook, Calendar, Reminders, Photos, SystemPolicyAllFiles, SpeechRecognition, …) on the macOS release you target.TCC.db directly is unsupported. Treat the SQLite file as an implementation detail that may move, change schema, be sealed by SIP, or require Full Disk Access to even open. "Zero rows in access" is not a reliable success signal — a visible fresh prompt at next launch is.Skip the broad tccutil reset All and the All + no-bundle forms. They touch more than the suite under test and leave you debugging state you did not intend to change. A targeted reset per service per bundle is the only form this article recommends.
Save the script below as scripts/tcc-reset.sh. It runs on the macOS default shell (Bash 3.2), targets one bundle ID, resets only the services you list, and leaves a human-readable audit line per call. There is no declare -A (Bash 4-only), no sudo, no identity switching, no TCC.db query.
#!/bin/bash
# tcc-reset.sh — reset TCC services for one test bundle (Bash 3.2 compatible)
# Usage: scripts/tcc-reset.sh com.example.yourtestapp [audit.log]
set -eu
BUNDLE_ID="${1:?Usage: $0 <bundle-id> [audit-log]}"
AUDIT_LOG="${2:-reset-audit.log}"
# Edit this list to the services your test bundle actually uses.
# Confirm names against `man tccutil` on your target macOS version.
SERVICES="Camera Microphone AddressBook Calendar Reminders Photos SpeechRecognition"
: > "$AUDIT_LOG"
printf '=== tcc-reset %s @ %s ===\n' \
"$BUNDLE_ID" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" | tee -a "$AUDIT_LOG"
for svc in $SERVICES; do
ts=$(date -u +%H:%M:%SZ)
if tccutil reset "$svc" "$BUNDLE_ID" >>"$AUDIT_LOG" 2>&1; then
printf '[%s] %-20s OK\n' "$ts" "$svc" | tee -a "$AUDIT_LOG"
else
printf '[%s] %-20s FAIL (unknown service on this macOS?)\n' \
"$ts" "$svc" | tee -a "$AUDIT_LOG"
exit 1
fi
done
printf '=== reset complete; verify each service with a fresh-prompt check ===\n' \
| tee -a "$AUDIT_LOG"
This is an unexecuted example. Run it once by hand on your macOS target, confirm each service name is accepted on that release, then wire it in. Use a disposable test bundle ID — not the production app identifier — so a failed reset never corrupts a developer's own permission state.
Fresh-prompt verification matrix. After the script runs, the only honest proof the reset worked is that the system prompt appears again at next launch. Keep this matrix next to the suite and tick each row during a dry run:
| Service (tccutil arg) | Trigger in the test app | Expected after reset |
|---|---|---|
Camera | AVCaptureDevice.requestAccess(for: .video) | System dialog shown |
Microphone | AVCaptureDevice.requestAccess(for: .audio) | System dialog shown |
AddressBook | CNContactStore.requestAccess | System dialog shown |
Calendar | EKEventStore.requestFullAccessToEvents | System dialog shown |
Reminders | EKEventStore.requestFullAccessToReminders | System dialog shown |
Photos | PHPhotoLibrary.requestAuthorization | System dialog shown |
SpeechRecognition | SFSpeechRecognizer.requestAuthorization | System dialog shown |
If a row stays silent, the reset did not take effect for that service on that macOS version. Re-check the exact service name in man tccutil, confirm the running process really is your test bundle, and quit and relaunch — TCC caches the decision in-process, so a long-running app can keep acting on a stale answer.
GitHub Actions self-hosted macOS runner. Call the script from the job and upload the audit log so each build keeps a record of which services were reset:
- name: Reset TCC for test bundle
env:
BUNDLE_ID: com.example.yourtestapp
run: |
chmod +x scripts/tcc-reset.sh
scripts/tcc-reset.sh "$BUNDLE_ID" reset-audit.log
- uses: actions/upload-artifact@v4
if: always()
with:
name: tcc-reset-audit
path: reset-audit.log
Self-hosted runners do not get the clean-VM guarantee of GitHub-hosted runners; permission state can survive between jobs. GitHub's current hardening guidance for self-hosted runners recommends ephemeral just-in-time runners and tight access control. Pair that posture with the reset step so the suite starts from a known state even on a long-lived runner.
Xcode Cloud. Xcode Cloud reads its custom build scripts from a ci_scripts/ folder at the repository root — not from the repo root itself. Create ci_scripts/ci_pre_xcodebuild.sh, mark it executable, and invoke the reset script from there:
#!/bin/sh
# ci_scripts/ci_pre_xcodebuild.sh
set -eu
"$CI_WORKSPACE/scripts/tcc-reset.sh" "com.example.yourtestapp" \
"$CI_DERIVED_DATA_PATH/reset-audit.log"
Xcode Cloud runs on Apple-managed hosts; privacy prompts outside the simulator cannot be answered interactively there, so keep tests that require a live Allow tap on a Mac you own or rent, and use simulator-driven paths for the hosted service.
Request a configuration quote at /mac-hosting/start and describe the exact services your suite resets, the macOS version you target, and whether tests need a live permission tap or run only in the simulator.