Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Full-Disk Access Automation Test on macOS

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.

Full Disk Access and the TCC service identifier

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.

Removing FDA with tccutil

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")
    }
}

Running the helper and asserting the NSError

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.

Re-granting FDA: SIP and the two-phase approach

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:

  • SIP disabled on a dedicated test host — only suitable for a machine that is not a developer's daily driver, since SIP removes a layer of kernel-level protection. Verify behaviour on the exact macOS build before relying on any TCC-write technique.
  • MDM configuration profile — in a managed fleet, a Privacy Preferences Policy Control (PPPC) payload can grant FDA without SIP changes. Appropriate for enterprise CI.
  • Two separate jobs — one job resets FDA and asserts the error; a second job runs on a machine where FDA is pre-granted and asserts success. A reporting step compares the two JSON evidence files. This avoids touching SIP at all.

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)
}

Capturing the before/after as a JSON evidence attachment

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.

Picking a Figment tier for this workflow

PhaseHost requirementSuitable Figment tier
FDA denied + assert errormacOS + tccutil + XCTest$29 virtual Intel macOS, or request a physical i3 ($49) / M-series (from $95)
FDA granted + assert successFDA pre-granted (manual, MDM, or image)Any tier that your admin can pre-grant
Full cycle with tccutil writeSIP disabled on a dedicated hostPhysical 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:


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms