
If your macOS app reads the window list — for a screenshot tool, meeting overlay, accessibility helper, or screen recorder — you need an automated check that the screen-recording TCC gate is actually open on the runner before the suite starts. This guide shows how to assert permission state from XCUITest, fail loudly when the gate is closed, and export a JSON evidence file per test run.
Apple does not ship a supported command to grant a TCC decision from the shell: tccutil exposes only reset. Treat screen-recording permission as a one-time setup step on your runner (an interactive grant preserved in the TCC database, or an MDM/PPPC configuration profile on supervised fleets), then let the test suite enforce the grant on every run.
Since macOS Catalina, CGWindowListCopyWindowInfo does not prompt and does not throw when screen-recording permission is absent. It silently filters the metadata it returns: the kCGWindowName key is withheld from every window, and kCGWindowSharingState is set to zero for windows your app does not own.
That silent degradation is the common CI bug: the test passes because the call succeeds, but your app receives empty window names and cannot do its job.
The modern API surface for checking and requesting permission is on CGPreflightScreenCaptureAccess / CGRequestScreenCaptureAccess:
// Swift — check before reading the window list
if CGPreflightScreenCaptureAccess() {
// permission already granted; proceed
} else {
CGRequestScreenCaptureAccess()
// macOS shows the system prompt; a CI runner must pre-grant interactively or via PPPC
}
CGRequestScreenCaptureAccess() shows a UI prompt, which makes it unsuitable for a headless test leg. Pre-grant on the runner once, then use the preflight call as your gate.
tccutil only supports the reset verb; there is no public insert or add verb that writes a grant. Scripts that call tccutil insert ScreenCapture ... allowed will fail on current macOS. For a CI Mac you have two supported options:
xctest tool. Check the exact identity with codesign -dr - $(xcrun --find xctest); the bundle changes across Xcode versions.If you need to re-prompt during setup, tccutil reset ScreenCapture <bundle-id> is the supported verb. macOS 15 can show a weekly reminder sheet for screen capture; supervised fleets suppress it via PPPC, and on a solo runner you dismiss the sheet and re-verify the grant on the next run.
The test launches the app, calls CGPreflightScreenCaptureAccess(), then queries the window list and asserts at least one window has a non-empty name — only possible when the test host already holds the grant. If permission is missing, the test fails loudly rather than silently passing on filtered metadata.
import XCTest
import CoreGraphics
final class ScreenRecordingPermissionTests: XCTestCase {
func testWindowListReturnsNamesWithPermission() throws {
let app = XCUIApplication()
app.launch()
XCTAssertTrue(app.windows.firstMatch.waitForExistence(timeout: 5))
XCTAssertTrue(CGPreflightScreenCaptureAccess(),
"Screen Recording not granted to the test host. Grant it in System Settings "
+ "to the signed xctest tool (check with `codesign -dr - $(xcrun --find xctest)`).")
let windowList = CGWindowListCopyWindowInfo(
[.optionAll, .excludeDesktopElements],
kCGNullWindowID
) as? [[CFString: Any]] ?? []
XCTAssertFalse(windowList.isEmpty, "CGWindowListCopyWindowInfo returned no windows")
let namedCount = windowList.filter { dict in
(dict[kCGWindowName] as? String).map { !$0.isEmpty } ?? false
}.count
XCTAssertGreaterThan(namedCount, 0,
"No window names returned — screen-recording permission is likely absent")
let evidence: [String: Any] = [
"totalWindowCount": windowList.count,
"namedWindowCount": namedCount,
"permissionGranted": namedCount > 0,
"timestamp": ISO8601DateFormatter().string(from: Date())
]
try writeEvidenceJSON(evidence)
}
private func writeEvidenceJSON(_ dict: [String: Any]) throws {
let data = try JSONSerialization.data(
withJSONObject: dict,
options: [.prettyPrinted, .sortedKeys]
)
let dir = ProcessInfo.processInfo.environment["EVIDENCE_DIR"] ?? NSTemporaryDirectory()
let url = URL(fileURLWithPath: dir)
.appendingPathComponent("screen-recording-evidence.json")
try data.write(to: url, options: .atomic)
print("Evidence written to \(url.path)")
}
}
Attach the evidence file to the xcresult bundle via XCTAttachment or have your CI step copy it next to TestResults.xcresult. Code above is an unexecuted illustration; run it against your own project and scheme before trusting the output.
Do not reset Screen Recording at the end of every run. Once the record is cleared there is no supported command to put the decision back without a prompt. The practical workflow for a dedicated CI Mac:
xctest tool.tccutil reset ScreenCapture <bundle> when you need to re-verify the prompt flow, and schedule an interactive re-grant immediately after.Both the app under test and the test host call CGWindowListCopyWindowInfo, so both must appear in the Screen Recording pane.
The Figment $29 virtual Intel macOS VM runs macOS on Linux/KVM without GPU acceleration or audio, so unit tests, compile steps, and headless logic exercise fine, but the window-server graph behind CGWindowListCopyWindowInfo is not the hardware-backed CoreGraphics stack your users run. For a faithful screen-recording TCC test, use a physical Mac.
A one-scenario worksheet for a solo iOS/macOS developer running this suite nightly:
| Option | Published price | Term | Notes |
|---|---|---|---|
| Figment M-series (entry) | from $95/mo | monthly, specs quoted on request | Managed CI add-ons start at $249/mo for one Mac with CI setup, backups, monitoring and 30 minutes of monthly support. |
| CloudMac M4, 16 GB, 256 GB | $99.99/mo | monthly | 256 GB row shows "Reserve now"; 512 GB and 1 TB show "Fully reserved". |
| CloudMac M4, 16 GB, 256 GB annual | $999/yr ($83.25/mo effective) | 12-month prepay | Cheaper than 12 × $95 = $1,140 at Figment's entry price; CloudMac quotes a specific M4/16 GB config, Figment's $95 entry is an unspecified M-series until quoted. |
If you need a verified M4 at 16 GB, CloudMac's annual plan is the lower sticker. Choose Figment when you want the specific managed CI work — image prep, signing keychain layout, runner registration, xcresult retention — bundled with the box, or when you need the $29 Intel VM for cheap pre-flight build/unit legs alongside the physical runner. Confirm exact chip, RAM, disk, availability, and any taxes or fees in the actual offer before you commit.
Request a configuration at /mac-hosting/start or review plans at /mac-hosting.