Lab note · from Mira
CMIO camera extension: personal teams can't sign System Extension
A Core Media I/O (CMIO) camera extension is a system extension, and the app that activates it has to be signed with the System Extension capability. Apple's capability table for macOS offers that capability to Apple Developer Program members and Developer ID, not to free accounts, so Xcode refuses to create the provisioning profile: "Personal development teams … do not support the System Extension capability." The rest of a virtual-camera app, meaning capture, recording and the frame loop, builds without the membership and runs as an unsigned preview. What doesn't work is the camera that other apps would see.
Symptoms
Mira is a menu bar app built to publish a virtual camera that plays a short recorded loop of you, so your video doesn't go dark when you step away from a call. It has two targets: the host app and a CMIO camera extension, generated by XcodeGen as a system-extension target.
- A build with automatic provisioning and the author's free team stopped at this, recorded in the project's handoff notes:
error: Cannot create a Mac App Development provisioning profile for "com.nosleeplab.mira".
Personal development teams, including "…", do not support the System Extension capability.
- Before that build, Xcode's account settings showed the team with
isFreeProvisioningTeam = 0, which read like a paid account. The notes record the lesson: only a real signed build settled it. - An ad-hoc build of the current commit succeeds with Xcode 27.0, with one concurrency warning. The extension is embedded at
Contents/Library/SystemExtensions/MiraCameraExtension.systemextension, and both bundles reportSignature=adhocandTeamIdentifier=not set. - So no "Mira Camera" for Photo Booth or FaceTime to pick: the extension has never been activated, and
systemextensionsctl liston the development Mac has no Mira entry.
Why it happens
Four pieces of Apple documentation make the chain.
It's a system extension. Apple's camera extension article: "Camera extensions are a new type of system extension available in macOS 12.3 and later." The app target needs two capabilities: "System Extension. This capability enables the app to install system extensions" and App Groups, so the app and extension can share a container. Its example entitlements:
<key>com.apple.developer.system-extension.install</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>$(TeamIdentifierPrefix)com.example.CustomCamera</string>
</array>
The entitlement is restricted. That key is the entitlement that says "whether your app has permission to activate or deactivate system extensions". TN3125 lists the unrestricted entitlements a Mac app can claim without a profile: com.apple.security.get-task-allow, com.apple.security.application-groups, and the App Sandbox and Hardened Runtime entitlements. system-extension.install isn't among them, and "restricted entitlements must be authorized by a provisioning profile."
Free accounts don't get the capability. Apple's Supported capabilities (macOS) table has three columns: ADP, Developer ID, and Apple Developer, defined as "Apple Account holders who have agreed to the Apple Developer Agreement… No cost is associated with this agreement and developers can't distribute apps." System Extension is ticked for ADP and Developer ID and not for Apple Developer. App groups is ticked for all three, so System Extension is the only one of the two that needs the membership.
Activation checks it. Installing System Extensions and Drivers says the system verifies, among other things, "The code signature of your extension" and that "The entitlements in the code signature match the entitlements granted to your development team."
So an ad-hoc build has no profile to authorize the entitlement, since, as Quinn of Apple DTS puts it, "a profile can only authorise code signed with signing identity whose certificate was issued by Apple". And a free team can't get a profile that includes it.
The fix
The supported route is an Apple Developer Program membership. For Mira that means three changes, which commit cdc743a shows being removed for the local-only configuration: automatic signing with the paid team, the system-extension.install and team-prefixed App Groups entitlements restored, and CMIOExtensionMachServiceName set back to $(TeamIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER), the form the paid configuration uses.
Then activation. Mira's ExtensionManager, reduced to the platform calls:
let request = OSSystemExtensionRequest.activationRequest(
forExtensionWithIdentifier: extensionID, queue: .main)
request.delegate = self
OSSystemExtensionManager.shared.submitRequest(request)
func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) {
// waits until an admin allows it in System Settings › Privacy & Security
}
func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) {
switch (error as NSError).code {
case 3: break // OSSystemExtensionErrorUnsupportedParentBundleLocation: not in /Applications
case 8: break // OSSystemExtensionErrorCodeSignatureInvalid
default: break
}
}
Why each step is there:
- Apple's camera article: "Only apps that reside in the
/Applicationsdirectory can activate an extension." Mira maps code 3 to "Move Mira.app to /Applications, then try again", and code 8 to an invalid-certificate message. The codes come fromSystemExtensions.h. - The same article says "a person with Admin privileges for the Mac must explicitly allow access to it" in System Settings.
SystemExtensions.hsays the request "will remain pending until user approves, or until the application exits." - After approval the extension is "automatically available as a selectable camera in system apps like FaceTime and PhotoBooth", and to AVFoundation capture.
Mira submits the request on every launch while the status is unknown or not installed, and its Settings pane has Reinstall and Remove buttons that send activation and deactivation requests.
The free route, local only
Apple's debugging guide describes two relaxations for development. With systemextensionsctl developer on "the system doesn't check the location of your system extension prior to loading it", and disabling System Integrity Protection "bypasses the notarization checks". Mira's current configuration bets on these: ad-hoc signing with both entitlements removed. The project recorded systemextensionsctl developer on refusing to run while SIP was enabled. It was never tested past that. csrutil status on the development Mac still reports SIP enabled, and Apple's guide doesn't say either relaxation waives the entitlement check.
What works without the membership
- The whole project compiles, extension included.
./build.sh previewbuilds withCODE_SIGNING_ALLOWED=NO. Its binaries come out linker-signed ad hoc with no entitlements, so the preview runs outside the App Sandbox that the real build declares. The handoff notes mark live camera preview and camera switching as working in it, with recording and the clip library implemented.- The loop's ping-pong ordering passes 182 checks in
Tests/LogicChecks.swift, a standalone copy of the sequencing function. The loop's frames go only to the virtual camera's sink, though: the app's own preview shows the live camera, and the sink connection drops frames when nothing is connected. Without the extension, the loop runs and nothing shows it.
How to check you've fixed it
codesign -dvon the app and on the.systemextensionshows a team identifier, andcodesign -d --entitlements - Mira.appincludescom.apple.developer.system-extension.install. TN3125: "macOS expects to find the profile atMyApp.app/Contents/embedded.provisionprofile."- With the app in
/Applications, launch it and approve the request.systemextensionsctl listshould show it asactivated enabled, whichman systemextensionsctldescribes as "Available for use". - Enumerate cameras with
AVCaptureDevice.DiscoverySession. Apple's article uses.externalUnknown, which the SDK header now calls "A deprecated synonym for AVCaptureDeviceTypeExternal". Mira'sverify-camera.swiftdoes this and looks for "Mira" in the device names. Then pick it in Photo Booth or FaceTime.
Caveats
- Signing isn't the only unknown. The handoff notes say nothing in the extension or the host's sink connection "has been exercised at runtime". A membership removes the signing block, not the testing.
- The
$(TeamIdentifierPrefix)in Apple's App Groups example needs a team. Ad-hoc builds have none, which is why Mira's local configuration drops the prefix from both the group and the Mach service name. - Versions: camera extensions need macOS 12.3. Mira targets macOS 14.0 in Swift 5 language mode, was written against Xcode 26.5, and was rebuilt here with Xcode 27.0 and the macOS 27.0 SDK.
- The macOS capability table has no row for cameras. The block is the System Extension capability, and the entitlement's page says to add it "for all system extension types, including DriverKit extensions."
More lab notes
- AXIsProcessTrusted false after rebuild: ad-hoc signing and TCCAn ad-hoc signature's designated requirement is the build's cdhash, so changed code no longer matches its Accessibility grant. Sign with a stable identity.
- CGWindowListCreateImage unavailable in macOS 15: use SCStreamCGWindowListCreateImage is deprecated in macOS 14 and a compile error from a macOS 15 target. Anchor still uses it; here is the SCStream replacement.
- Check a password against Have I Been Pwned without sending itSHA-1 the password, send only the first 5 hex characters to the Pwned Passwords range API, match the suffix locally, and add Add-Padding: true.
- Claude Code signed out by another app: refresh token rotationAnthropic rotates the refresh token on every refresh, so another app that refreshes Claude Code's login signs it out. Only Claude Code should refresh it.