preflight-permissions
Preflight Permissions
Ensure all macOS system permissions are granted before automated UI tests run. This prevents XCUITests from hanging on permission dialogs or failing silently when TCC blocks access.
When to Use
Run this skill once when starting a new project, after cloning, or whenever tests fail
with permission-related errors. It is a prerequisite for journey-builder and journey-loop.
What It Does
- Sets up a self-signed code signing certificate (so permissions persist across rebuilds)
- Builds the app and test runner with that certificate
- Detects which TCC permissions the app needs
- Guides the user to grant each permission in System Settings
- Runs a smoke XCUITest to verify permissions work
- Reports pass/fail for each permission
Step 1: Detect Project Configuration
Read project.yml (XcodeGen) or scan *.xcodeproj for:
- App bundle ID (e.g.,
com.percev.app) - UI test target name (e.g.,
PercevUITests) - App target name (e.g.,
Percev) - Existing
CODE_SIGN_IDENTITY— if already set to something other than"-", skip certificate creation - Entitlements file — check for existing entitlements
Report what was found before proceeding.
Step 2: Create Self-Signed Certificate
Use the /macos-codesign skill approach. The certificate name MUST be "{AppName} Dev" (e.g., "Percev Dev").
CERT_NAME="{AppName} Dev"
# Check if it already exists
if security find-identity -v -p codesigning 2>/dev/null | grep -q "$CERT_NAME"; then
echo "Certificate '$CERT_NAME' already exists. Skipping creation."
else
echo "Creating self-signed code signing certificate '$CERT_NAME'..."
echo ">>> You may see a Keychain Access dialog — approve it once. <<<"
cat > /tmp/cert.cfg <<CERT_EOF
[ req ]
distinguished_name = req_dn
[ req_dn ]
CN = $CERT_NAME
[ extensions ]
keyUsage = digitalSignature
extendedKeyUsage = codeSigning
CERT_EOF
openssl req -x509 -newkey rsa:2048 \
-keyout /tmp/dev.key -out /tmp/dev.crt \
-days 3650 -nodes \
-config /tmp/cert.cfg -extensions extensions \
-subj "/CN=$CERT_NAME" 2>/dev/null
security import /tmp/dev.crt -k ~/Library/Keychains/login.keychain-db -T /usr/bin/codesign 2>/dev/null
security import /tmp/dev.key -k ~/Library/Keychains/login.keychain-db -T /usr/bin/codesign 2>/dev/null
security add-trusted-cert -d -r trustRoot -k ~/Library/Keychains/login.keychain-db /tmp/dev.crt 2>/dev/null
rm -f /tmp/cert.cfg /tmp/dev.key /tmp/dev.crt
echo "Certificate '$CERT_NAME' created and trusted."
fi
# Verify it exists
security find-identity -v -p codesigning | grep "$CERT_NAME"
If the certificate was just created or CODE_SIGN_IDENTITY is "-", update the project:
If project.yml exists (XcodeGen):
- Change
CODE_SIGN_IDENTITY: "-"toCODE_SIGN_IDENTITY: "{AppName} Dev" - Run
xcodegen generateto regenerate the Xcode project
NEVER edit .xcodeproj manually. If no project.yml exists, create one first with xcodegen.
Step 3: Build the App
Build the app target to produce a signed binary. Always use -derivedDataPath build so the .app lands in the project root at a predictable path (build/Build/Products/Debug/{AppName}.app). This lets the user easily find and run the app to grant permissions.
xcodebuild build \
-project {Project}.xcodeproj \
-scheme {AppName} \
-destination 'platform=macOS' \
-derivedDataPath build \
-quiet \
2>&1
After a successful build, print the app path so the user knows where it is:
echo "Built app: $(pwd)/build/Build/Products/Debug/{AppName}.app"
If the build fails, diagnose and fix before continuing. Common issues:
- Certificate not trusted → re-run
security add-trusted-cert - Keychain locked →
security unlock-keychain ~/Library/Keychains/login.keychain-db
Step 4: Detect Required Permissions
Read the app's entitlements file and source code to determine which TCC permissions are needed:
| Permission | How to Detect | System Settings Path |
|---|---|---|
| Screen Recording | Entitlement com.apple.security.screen-capture OR uses ScreenCaptureKit/CGWindowList |
Privacy & Security > Screen Recording |
| Microphone | Entitlement com.apple.security.device.audio-input OR uses AVCaptureDevice for audio |
Privacy & Security > Microphone |
| Accessibility | Uses AXIsProcessTrusted() or Accessibility APIs |
Privacy & Security > Accessibility |
| Automation | XCUITest needs Accessibility access to control the app | Privacy & Security > Accessibility |
| Full Disk Access | App reads/writes files outside its container (e.g., ~/AppName/, /tmp/ test fixtures). Without this, macOS shows "would like to access data of other apps" dialog on every launch, blocking unattended UI tests. |
Privacy & Security > Full Disk Access |
Also check:
grep -r "SCShareableContent\|SCStreamConfiguration\|CGWindowListCreate" {SourceDir}/for Screen Recordinggrep -r "AVCaptureDevice\|AVAudioSession\|microphone" {SourceDir}/for Microphonegrep -r "AXIsProcessTrusted\|AXUIElement" {SourceDir}/for Accessibility- Check if the app accesses user-home paths (e.g.,
~/AppName/) or/tmp/directories for test fixtures — if so, Full Disk Access is required
Build a checklist of required permissions.
Step 5: Guide User to Grant Permissions
For each required permission, tell the user exactly what to do:
=== PERMISSIONS NEEDED ===
The following permissions must be granted ONCE in System Settings.
After granting, they will persist across rebuilds (thanks to the code signing certificate).
1. [ ] Screen Recording
→ System Settings > Privacy & Security > Screen Recording
→ Add: {AppName} (find it in the app list or use "+")
2. [ ] Microphone
→ System Settings > Privacy & Security > Microphone
→ Toggle ON for {AppName}
3. [ ] Accessibility (for XCUITest automation)
→ System Settings > Privacy & Security > Accessibility
→ Add: Xcode (if not already present)
→ Add: {AppName}
4. [ ] Full Disk Access (prevents "access data of other apps" dialog)
→ System Settings > Privacy & Security > Full Disk Access
→ Add: {AppName}
→ Also add: Xcode.app and/or xcodebuild (if running tests from CLI)
→ Without this, a blocking dialog appears on EVERY app launch during tests
Open System Settings now:
open "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"
open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"
IMPORTANT: Open each relevant System Settings pane automatically using open commands. Wait for the user to confirm they've granted permissions before proceeding.
Launch the app once so it appears in the TCC permission lists:
# Launch the built app briefly so macOS registers it for TCC permissions
APP_PATH="$(pwd)/build/Build/Products/Debug/{AppName}.app"
echo "Launching $APP_PATH so it appears in System Settings permission lists..."
open "$APP_PATH"
sleep 3
osascript -e 'tell application "{AppName}" to quit'
Tip for the user: You can also run the app manually any time with:
open build/Build/Products/Debug/{AppName}.app
Step 6: Verify with Smoke Test
Write a minimal XCUITest that exercises permission-dependent features:
import XCTest
final class PermissionSmokeTests: XCTestCase {
let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
app.launch()
}
override func tearDownWithError() throws {
app.terminate()
}
func testAppLaunchesAndWindowExists() throws {
// Verify the app launches without permission dialogs blocking it
let window = app.windows.firstMatch
XCTAssertTrue(window.waitForExistence(timeout: 10),
"App window should appear — if stuck, check Accessibility permission")
// Take a screenshot to verify no permission dialog is blocking
let screenshot = window.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = "preflight-001-app-launched"
attachment.lifetime = .keepAlways
add(attachment)
}
func testNoPermissionDialogsOnScreen() throws {
// Check that no system dialog is blocking the app
let window = app.windows.firstMatch
XCTAssertTrue(window.waitForExistence(timeout: 10))
// On macOS, check for alerts/sheets on the app itself (NOT springboard — that's iOS only)
let alert = app.alerts.firstMatch
XCTAssertFalse(alert.waitForExistence(timeout: 3),
"Permission alert detected — grant the permission in System Settings first")
let sheet = app.sheets.firstMatch
XCTAssertFalse(sheet.waitForExistence(timeout: 2),
"Permission sheet detected — grant the permission in System Settings first")
}
}
Place this test in the UI test target if it doesn't already exist. Run it:
xcodebuild test \
-project {Project}.xcodeproj \
-scheme {UITestScheme} \
-destination 'platform=macOS' \
-derivedDataPath build \
-only-testing:{UITestTarget}/PermissionSmokeTests \
-resultBundlePath /tmp/preflight-results.xcresult \
-quiet \
2>&1
Step 7: Report
Output a clear status report:
=== PREFLIGHT PERMISSIONS REPORT ===
Certificate: ✅ {AppName} Dev (persists across rebuilds)
Build: ✅ Signed with {AppName} Dev
Screen Recording: ✅ Granted (or ❌ NOT granted — tests will hang)
Microphone: ✅ Granted (or ⚠️ Not needed / ❌ NOT granted)
Accessibility: ✅ Granted (or ❌ NOT granted — XCUITest will fail)
Full Disk Access: ✅ Granted (or ❌ NOT granted — "access data" dialog blocks every launch)
Smoke Test: ✅ Passed (or ❌ Failed — see errors above)
Status: READY FOR AUTOMATED TESTING
(or: BLOCKED — fix the items marked ❌ above)
If all checks pass, the project is ready for journey-builder and journey-loop.
Rules
- NEVER skip the certificate step — ad-hoc signing causes permission revocation on every rebuild
- NEVER try to programmatically grant TCC permissions — only the user can do this in System Settings
- ALWAYS open the relevant System Settings pane automatically for the user
- ALWAYS launch the app once before asking the user to grant permissions (so it appears in TCC lists)
- ALWAYS run the smoke test to verify — don't trust "I granted it" without a passing test
- If the smoke test fails, diagnose whether it's a permission issue or a build issue before asking the user to re-grant
- This skill modifies
project.ymland/or build settings — commit these changes so the team benefits
More from sunfmin/autocraft
autocraft
>
20journey-builder
Build and test the longest uncovered user journey from spec.md. Reads the product spec, checks existing journeys, picks the longest untested path, writes a UI test with screenshots at every step, then runs 3 polish rounds (testability → refactor UI test → UI review) until everything is clean. Use when the user says "next journey", "add journey", "test the next flow", "journey builder", or "cover more user paths".
4journey-loop
Orchestrates a continuous journey-builder → refine → restart loop. Runs journey-builder and refine-journey sequentially, improving the skill each iteration. Loops until all spec requirements are covered by journeys and the score reaches 95%.
4refine-journey
Evaluate the output of a journey-builder run, identify instruction gaps, and edit the project root AGENTS.md (or add pitfalls to the gist) to fix those gaps. Does NOT modify the journey-builder skill itself.
3