A complete guide for implementing privileged helper daemons using Apple's modern SMAppService API on macOS 15+. This repository provides a working example and step-by-step instructions to avoid common configuration pitfalls.
Reference implementation for keyboard remapping and system-level operations
This guide helps you implement a privileged helper daemon that can:
- Register with SMAppService - Use Apple's modern service management API
- Execute privileged operations - Run tasks requiring root permissions
- Communicate via XPC - Secure inter-process communication
- Work on macOS 15+ - Compatible with Sequoia and later
This error typically occurs when mixing legacy SMJobBless configuration with modern SMAppService:
Root Causes:
- Mixed APIs: Using
SMAuthorizedClients/SMPrivilegedExecutables(SMJobBless) with SMAppService - Missing .plist extension:
daemon(plistName:)requires the full filename including extension
Solutions: See Configuration Guide below.
This repository includes servicemanagement-updating-helper-executables-from-earlier-versions-of-macos.md - a markdown version of Apple's documentation generated using llm.codes by @steipete.
Why this matters: Apple's documentation requires JavaScript, making it difficult for LLMs to parse. The markdown version enables AI assistants to provide accurate guidance based on official Apple documentation.
Generate your own: Use @steipete's llm.codes tool to convert any Apple documentation into LLM-readable markdown format.
Info.plist - No SMAppService-specific keys needed:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Standard app keys only - no SMPrivilegedExecutables -->
</dict>
</plist>Entitlements (helperpoc.entitlements):
<key>com.apple.security.app-sandbox</key>
<false/>Swift Implementation:
// CRITICAL: Include .plist extension
private let helperPlistName = "com.keypath.helperpoc.helper.plist"
let service = SMAppService.daemon(plistName: helperPlistName)
try service.register()Info.plist - Standard bundle keys only:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.keypath.helperpoc.helper</string>
<key>CFBundleName</key>
<string>helperpoc-helper</string>
<!-- No SMAuthorizedClients with SMAppService -->
</dict>
</plist>Entitlements (helperpoc-helper.entitlements):
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.developer.service-management.managed-by-main-app</key>
<true/>Daemon Configuration (com.keypath.helperpoc.helper.plist):
<dict>
<key>Label</key>
<string>com.keypath.helperpoc.helper</string>
<key>BundleProgram</key>
<string>Contents/MacOS/helperpoc-helper</string>
<key>AssociatedBundleIdentifiers</key>
<array>
<string>com.keypath.helperpoc</string> <!-- Main app identifier -->
</array>
<key>MachServices</key>
<dict>
<key>com.keypath.helperpoc.xpc</key>
<true/>
</dict>
</dict>helperpoc.app/
βββ Contents/
β βββ MacOS/
β β βββ helperpoc # Main app binary
β β βββ helperpoc-helper # Helper daemon binary
β βββ Library/
β βββ LaunchDaemons/
β βββ com.keypath.helperpoc.helper.plist
- macOS 15.x Sequoia or later
- Xcode 16.x
- Apple Developer account with Developer ID certificates
# Clone repository
git clone https://github.com/malpern/privileged_helper_help.git
cd privileged_helper_help/helperpoc
# Build project
xcodebuild -project helperpoc.xcodeproj -scheme helperpoc -configuration Debug clean build
# Launch app
open build/Debug/helperpoc.app- Click "Register Helper" - May initially show "Operation not permitted"
- Approve in System Settings - macOS will prompt for permission
- Verify "Helper Status: Enabled" - Registration successful
- Test functionality - Click "Test Helper" to verify privileged operations
Error 108 "Unable to read plist":
- β
Remove
SMAuthorizedClientsfrom helper's Info.plist - β
Remove
SMPrivilegedExecutablesfrom main app's Info.plist - β
Add
.plistextension todaemon(plistName:)parameter - β
Verify
AssociatedBundleIdentifiersreferences main app
"Operation not permitted":
- β Normal first-time behavior - approve in System Settings
- β Check System Settings > General > Login Items & Extensions
Build failures:
- β Ensure helper binary is copied to correct bundle location
- β
Verify daemon plist is embedded in
Contents/Library/LaunchDaemons/
| Feature | SMJobBless (Legacy) | SMAppService (Modern) |
|---|---|---|
| macOS Version | 10.6-13.0 | 13.0+ |
| Configuration | SMAuthorizedClients/SMPrivilegedExecutables | Bundle structure only |
| Helper Location | Contents/Library/LaunchServices | Contents/Library/LaunchDaemons |
| Plist Reference | Without extension | With .plist extension |
| Authorization | Manual code signing validation | Automatic with proper bundle |
| API Status | Deprecated | Current best practice |
The helper implements a simple XPC protocol for secure communication:
@objc protocol HelperProtocol {
func createTestFile(reply: @escaping (Bool, String?) -> Void)
}- App logs: Check console for SMAppService registration messages
- Helper logs: Monitor system logs for daemon execution
- Bundle verification: Use
codesign -vvvto verify signatures
- Code signing: Use Developer ID Application certificates
- Notarization: Required for distribution outside Mac App Store
- User approval: Always required for first-time registration
- Quinn "The Eskimo!" @ Apple Developer Technical Support - For identifying the root causes in this Apple Developer Forums thread
- @steipete - For creating llm.codes which enabled AI-assisted debugging with proper Apple documentation
- Apple Developer Forums Thread - Complete troubleshooting discussion
- Kanata Keyboard Remapper - Target integration project
- llm.codes Documentation Tool - Convert Apple docs for AI assistance
This guide demonstrates working SMAppService implementation on macOS 15. For questions or issues, refer to Apple's official documentation or the developer forums.