
Buying recommendation. For the denied-FDA half of this test, Figment's $29 virtual Intel macOS instance is sufficient — it runs macOS with tccutil and XCTest, and compatible Intel toolchains (Xcode 26 or earlier, per Apple's Apple silicon porting guide) can drive this scenario. For a full deny/grant cycle on one machine, SIP must be disabled, so request a physical Mac on the i3 ($49/month) or M-series (from $95/month, with the exact chip, RAM and disk confirmed in the quote). Start a configuration at /mac-hosting/start. Reference APIs below are NSError and the App Sandbox / TCC overview; the physical i3 and M-series tiers are request-based until Figment confirms supply.
macOS enforces Full Disk Access (FDA) through the Transparency, Consent, and Control (TCC) subsystem. When an app or helper tool tries to read a protected path — ~/Library/Mail, ~/Library/Safari, parts of ~/Library/Application Support, or system locations under /private — the TCC daemon checks the database for a grant against the service identifier kTCCServiceSystemPolicyAllFiles. The string form used with tccutil is SystemPolicyAllFiles.
If no grant exists, the read fails. On the macOS versions we checked, the resulting error is typically in NSCocoaErrorDomain with code NSFileReadNoPermissionError (257), or in NSPOSIXErrorDomain with EACCES (13) when the helper surfaces the underlying errno. Verify the numeric value against the headers in your installed Xcode:
open $(xcrun --show-sdk-path)/System/Library/Frameworks/Foundation.framework/Headers/FoundationErrors.h
Your helper should surface a specific documented error rather than returning a generic failure — that specificity is what this test pins down.
tccutil reset clears TCC grants. It operates on the user-writable portion of the TCC store, so it does not require SIP to be disabled:
tccutil reset SystemPolicyAllFiles com.example.HelperApp
After this command, the next FDA-gated operation from that bundle ID has no grant row and will fail. Calling it in setUpWithError() makes every test run start from a known denied state.
override func setUpWithError() throws {
let proc = Process()
proc.executableURL = URL(fileURLWithPath: "/usr/bin/tccutil")
proc.arguments = ["reset", "SystemPolicyAllFiles", "com.example.HelperApp"]
try proc.run()
proc.waitUntilExit()
guard proc.terminationStatus == 0 else {
throw XCTSkip("tccutil reset failed; verify bundle ID and host permissions")
}
}
With FDA cleared, launch the helper and assert the error contract. The pattern below runs the helper as a subprocess, reads a JSON-encoded error payload from its standard error, and decodes it:
func testHelperReportsMissingFDAError() throws {
let (domain, code) = try runHelperAndCaptureError()
XCTAssertEqual(domain, NSCocoaErrorDomain,
"Helper must report NSCocoaErrorDomain for a file permission error")
XCTAssertEqual(code, NSFileReadNoPermissionError,
"Error code must be NSFileReadNoPermissionError (257) when FDA is denied")
attachErrorEvidence(phase: "fda_denied",
domain: domain, code: code, to: self)
}
private func runHelperAndCaptureError() throws -> (domain: String, code: Int) {
let helperURL = Bundle(for: Self.self)
.url(forResource: "HelperApp", withExtension: nil)!
let pipe = Pipe()
let proc = Process()
proc.executableURL = helperURL
proc.standardError = pipe
try proc.run()
proc.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
guard let json = try? JSONDecoder().decode(ErrorPayload.self, from: data) else {
throw XCTSkip("Helper did not produce a JSON error payload on stderr")
}
return (json.domain, json.code)
}
private struct ErrorPayload: Decodable {
let domain: String
let code: Int
}
The test treats the helper as a black box: it only asserts the public error contract, not any internal implementation detail. This is an unexecuted example — run it against your own helper and confirm the exact code your code paths emit.
tccutil reset removes grants; it cannot add them. Programmatically inserting an FDA grant means writing to the TCC database, which System Integrity Protection (SIP) protects from direct modification even by root. Disabling SIP requires rebooting into Recovery and running csrutil disable from the Recovery terminal; it cannot be done from a running macOS session. The TCC schema is not a documented public interface and changes between releases have broken third-party scripts.
In practice, pick one of:
For most teams the split-job pattern is simpler. The granted-state half:
func testHelperSucceedsWithFDA() throws {
// Must run on an FDA-granted host. Guard against misrouting:
let probe = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/Safari/History.db")
guard FileManager.default.isReadableFile(atPath: probe.path) else {
throw XCTSkip("FDA not granted on this host; route to the granted CI agent")
}
let (domain, code) = try runHelperSuccessResult()
XCTAssertNil(domain, "No error expected when FDA is granted")
XCTAssertEqual(code, 0, "Helper should exit 0 after a protected-file read")
attachErrorEvidence(phase: "fda_granted",
domain: domain ?? "none", code: code, to: self)
}
The unique artifact ties both phases together as one JSON file per run, stored inside the .xcresult bundle:
func attachErrorEvidence(phase: String,
domain: String,
code: Int,
to test: XCTestCase) {
let payload: [String: Any] = [
"phase": phase,
"domain": domain,
"code": code,
"timestamp": ISO8601DateFormatter().string(from: Date()),
"bundleID": "com.example.HelperApp"
]
guard let data = try? JSONSerialization.data(
withJSONObject: payload, options: .prettyPrinted) else { return }
let attachment = XCTAttachment(data: data,
uniformTypeIdentifier: "public.json")
attachment.name = "fda-evidence-\(phase).json"
attachment.lifetime = .keepAlways
test.add(attachment)
}
fda-evidence-fda_denied.json and fda-evidence-fda_granted.json land together in the result bundle and form an auditable before/after record for change management or security review.
| Phase | Host requirement | Suitable Figment tier |
|---|---|---|
| FDA denied + assert error | macOS + tccutil + XCTest | $29 virtual Intel macOS, or request a physical i3 ($49) / M-series (from $95) |
| FDA granted + assert success | FDA pre-granted (manual, MDM, or image) | Any tier that your admin can pre-grant |
Full cycle with tccutil write | SIP disabled on a dedicated host | Physical i3 or M-series, request-based |
The $29 VM runs macOS, so it can drive the denied-state job today — you don't need a physical Mac just to assert the missing-permission error. For the SIP-disabled or MDM-managed branches, request a physical configuration and ask Figment to confirm whether SIP can be toggled in Recovery on the delivered unit as part of the quote. If you also want Figment to set up and operate the two CI runners, backups and monitoring for this pipeline, the Managed Build Mac ($249/month, one M-series Mac with 30 minutes of monthly support) and Managed Mac Team ($999/month, two M-series Macs and two CI runners with one hour of monthly support) cover that work. Start at /mac-hosting/start or review tiers at /mac-hosting.
Related guides: