Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

macOS Microphone Entitlement Test Script

Run this test on a Figment M-series Mac from $95/month — the microphone TCC path and the audio-input entitlement are macOS features that need real macOS audio hardware. The $29 virtual Intel macOS plan runs macOS and is fine for compiling and verifying the Info.plist/entitlement declarations with Xcode 26 or earlier, but it has no audio device, so AVCaptureDevice.requestAccess(for: .audio) cannot exercise the full prompt and grant flow there. Exact chip, RAM and disk for the M-series plan are confirmed in the quote; physical plans collect requests until supply is confirmed. Start at Figment Mac hosting or request a configuration.

A macOS app that records audio needs two independent declarations before it touches the microphone: an NSMicrophoneUsageDescription string in Info.plist and a com.apple.security.device.audio-input entitlement in the signed entitlements file. If either is missing the hardened runtime denies the request before any TCC row is written and the user never sees a prompt. This guide gives you an XCTest helper that checks both, calls AVCaptureDevice.requestAccess(for:) to drive the real permission flow, reads the resulting TCC row, and exports the state as a JSON XCTAttachment that travels with every build artefact.

NSMicrophoneUsageDescription and the audio-input entitlement

Two declarations govern microphone access on macOS. In Info.plist:

<key>NSMicrophoneUsageDescription</key>
<string>MyApp uses the microphone to record audio notes.</string>

In the app's entitlements file (for example MyApp.entitlements):

<key>com.apple.security.device.audio-input</key>
<true/>

The description text appears in the system permission dialog. The entitlement is a hardened-runtime gate: without it, macOS rejects the request at the policy layer, before the user ever sees a prompt, and no TCC row is created. Your test must check both.

Triggering AVCaptureDevice.requestAccess in a test target

AVCaptureDevice.requestAccess(for: .audio) is an async API. It presents the system prompt on the first call and reads the cached TCC answer afterwards. Call it from a test method with async/await:

// MicrophoneEntitlementTests.swift
import XCTest
import AVFoundation

struct EntitlementResult: Codable {
    let bundleID: String
    let status: String       // "authorized" | "denied" | "restricted" | "notDetermined"
    let tccRowFound: Bool
    let timestamp: String
}

final class MicrophoneEntitlementTests: XCTestCase {

    let bundleID = "com.example.MyApp"

    func testMicrophoneEntitlementIsAuthorized() async throws {
        // 1. Verify the Info.plist key is present in the host app bundle.
        //    Bundle.main points at the host app when the test scheme sets one.
        let desc = Bundle.main.object(forInfoDictionaryKey: "NSMicrophoneUsageDescription")
        XCTAssertNotNil(desc,
            "NSMicrophoneUsageDescription must be set in the host app's Info.plist")

        // 2. Request access (presents the prompt on first run).
        let granted = await AVCaptureDevice.requestAccess(for: .audio)
        XCTAssertTrue(granted,
            "Microphone access was not granted. " +
            "Check the com.apple.security.device.audio-input entitlement.")

        // 3. Read the cached authorization status.
        let status = AVCaptureDevice.authorizationStatus(for: .audio)
        let statusString: String
        switch status {
        case .authorized:    statusString = "authorized"
        case .denied:        statusString = "denied"
        case .restricted:    statusString = "restricted"
        case .notDetermined: statusString = "notDetermined"
        @unknown default:    statusString = "unknown"
        }

        let tccFound = queryTCCMicrophoneRow(for: bundleID)

        // 4. Build and attach the JSON artefact.
        let result = EntitlementResult(
            bundleID: bundleID,
            status: statusString,
            tccRowFound: tccFound,
            timestamp: ISO8601DateFormatter().string(from: Date())
        )
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        let data = try encoder.encode(result)

        let attachment = XCTAttachment(data: data, uniformTypeIdentifier: "public.json")
        attachment.name = "microphone-entitlement-\(bundleID).json"
        attachment.lifetime = .keepAlways
        add(attachment)

        // 5. Final assertion.
        XCTAssertEqual(statusString, "authorized",
            "Expected .authorized but got \(statusString). See attached JSON.")
    }
}

lifetime = .keepAlways keeps the JSON in the .xcresult bundle even when the test passes, so it survives as a per-build compliance record.

Reading the TCC.db grant row

macOS stores privacy decisions in an SQLite database at ~/Library/Application Support/com.apple.TCC/TCC.db (per-user). A small helper shells out to sqlite3:

import Foundation

/// Returns true if the user TCC.db has an authorized mic row for this bundle ID.
func queryTCCMicrophoneRow(for bundleID: String) -> Bool {
    let tccPath = ("~/Library/Application Support/com.apple.TCC/TCC.db" as NSString)
        .expandingTildeInPath
    guard FileManager.default.fileExists(atPath: tccPath) else { return false }

    let pipe = Pipe()
    let proc = Process()
    proc.launchPath = "/usr/bin/sqlite3"
    proc.arguments = [
        tccPath,
        // Parameter-bind at the shell layer: the bundleID comes from a trusted constant.
        "SELECT client FROM access WHERE service='kTCCServiceMicrophone'" +
        " AND client='\(bundleID)' AND auth_value=2 LIMIT 1;"
    ]
    proc.standardOutput = pipe
    proc.standardError = FileHandle.nullDevice
    proc.launch()
    proc.waitUntilExit()

    let output = String(
        data: pipe.fileHandleForReading.readDataToEndOfFile(),
        encoding: .utf8
    )?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
    return output == bundleID
}

Caveats worth stating in the test comments:

  • The per-user TCC.db is readable by processes running as the same user. The system-level /Library/Application Support/com.apple.TCC/TCC.db is not readable without elevated privileges, and SIP protects it.
  • The auth_value integer codes are not a documented public API. On current macOS releases 2 corresponds to authorized; verify before relying on it long-term.
  • Do not write to TCC.db from tests. Use tccutil reset to return to the uncached state.

Resetting microphone permission between runs

To force the prompt on the next run (or to simulate a fresh install), reset the mic grant for the bundle ID before the test:

# Quit the app under test first, otherwise macOS may re-write the cached value.
/usr/bin/tccutil reset Microphone com.example.MyApp

Wire this into an Xcode scheme pre-action (Edit Scheme → Test → Pre-actions) or as a CI step that runs before xcodebuild test. Do not script writes directly to the SQLite file.

Why the host matters: Simulator and VM limits

The microphone TCC chain — com.apple.security.device.audio-input, NSMicrophoneUsageDescription, the TCC.db row — is a macOS subsystem and does not exist in the iOS Simulator. Run with -destination 'platform=macOS' against your macOS app target, not an iOS/Simulator destination.

The Figment $29 virtual Intel macOS plan runs macOS and can build and link a mic-capable macOS app with Xcode 26 or earlier (Xcode 27 requires Apple silicon per Apple's porting guidance). What it cannot do is emulate a real audio input device: without a mic, the .audio request has no hardware to authorize against. Use the M-series plan for the end-to-end entitlement and TCC grant assertions in this test; keep the $29 VM for build-time plist/entitlement checks and Intel-compatible compilation.

Picking a Figment plan, with the market beside it

For this single-app test the right starting point is one M-series Mac at $95/month. Exact chip, RAM and disk are set in the quote; the $95 is an entry price, not a committed M4/16 GB spec. Entry-level comparisons on monthly list:

  • Figment M-series Mac — from $95/month (configuration confirmed in the quote; request-based). Annual not published.
  • CloudMac M4 / 16 GB / 256 GB — $99.99/month, and the CloudMac home page advertises $999/year (≈ $83.25/month effective, about $141 less than 12 × $95). Larger disk tiers listed as fully reserved on the pricing page at time of check; the 256 GB row shows Reserve now.
  • MacStadium M2.S (8 GB / 256 GB) — $109/month; M4.S (16 GB / 256 GB) — $149/month. MacStadium publishes a 99.99% uptime target and includes support and network on its pricing page.

Those are lower entry-price M-series options on equivalent hardware for the single-mic-test job, and this page is not going to hide them. What a Figment Managed Build at $249/month adds over a bare M-series rental is the specific managed work in our offer: one M-series Mac, CI setup, backups, monitoring and 30 minutes of monthly support — worth it when you want the TCC-reset pre-action, the JSON attachment pipeline and the xcodebuild test runner wired into a working CI rather than left as a scheme change. The Managed Mac Team at $999/month gives two M-series Macs, two CI runners, team setup, backups, monitoring and one hour of monthly support for a small fleet.

When you know which fits, request a configuration and state the Xcode/macOS pair and whether you need the pre-action and JSON artefact wired up for you.


Sources checked 2026-11-10:


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms