My app is for personal use currently, so distribution won't be a problem. It registers a privileged helper using SMAppService, and I was wondering whether there is a way to customize the authorization dialog that the system presents to the user.
Service Management
RSS for tagThe Service Management framework provides facilities to load and unload launched services and read and modify launched dictionaries from within an application.
Posts under Service Management tag
60 Posts
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
I am developing the application in Mac. My requirement is to start the application automatically when user login.
I have tried adding the plist file in launch agents, But it doesn't achieve my requirement.
Please find the code added in the launch agents
<?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>Label</key>
<string>com.sftk.secure</string>
<key>ProgramArguments</key>
<array>
<string>/Applications/Testing.app/Contents/MacOS/Testing</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
</dict>
</plist>
I have tried by adding manually in the setting, but it was opened sometimes and closed suddenly. On open manually it works.
Please provide a solution to start the application automatically on system starts
our app has a helper to perform privileged operations.
previously that helper was installed via SMJobBless() into the /Library/LaunchDaemons/ and /Library/PrivilegedHelperTools/
we also had a script that would install the helper from the command-line, which was essential for enterprise users that could not manually install the helper on all their employee's Macs. the script would copy the files to their install location and would use launchctl bootstrap system as the CLI alternative to SMJobBless(). the full script is here: https://pastebin.com/FkzuAWwV
due to various issues with the old SMJobBless() approach we have ported to helper to the new SMAppService API where the helpers do not need to be installed but remain within the app bundle ( [[SMAppService daemonServiceWithPlistName:HELPER_PLIST_NAME] registerAndReturnError:&err] )
however, we are having trouble writing a (remote-capable) CLI script to bootstrap the new helper for those users that need to install the helper on many Macs at once. running the trivial
sudo launchctl bootstrap system /Applications/MacUpdater.app/Contents/Library/LaunchDaemons/com.corecode.MacUpdaterPrivilegedInstallHelperTool2.plist
would just result in a non-informative:
Bootstrap failed: 5: Input/output error
various other tries with launchctl bootstrap/kickstart/enable yielded nothing promising.
so, whats the command-line way to install a SMAppService based helper daemon? obviously 'installing' means both 'registering' (which we do with registerAndReturnError in the GUI app) and 'approving' (which a GUI user needs to manually do by clicking on the notification or by going into System Settings).
thanks in advance!
p.s. we wanted to submit this as a DTS TSI, but those are no longer available without spending another day on a reduced sample projects. words fail me.
p.p.s. bonus points for a CLI way to give FDA permissions to the app!
Topic:
App & System Services
SubTopic:
Core OS
Tags:
Entitlements
Service Management
Command Line Tools
Hello,
I want to create a Launch Agent that triggers an executable upon changes in the /Applications folder.
The launch agent is normally a loaded but not running, and by adding /Applications to the WatchPath parameters in the plist, launchd is supposed to trigger the process, that will run and exit once done.
Sadly this seems not to be working uniformly. The script only works on one machine, in the the others the execcutable is never run. There seem not to be any meaningful differences in the launchd or system logs.
The same identical plist works perfectly when changing something in the user's ~/Applications folder. The script does its job and logs are visible.
Is there an undocumented limitation specifically for the /Applications folder that prevents luanchd to observe it in the WatchPaths? Maybe SIP not allowing access? But why does it work on my machine?
Here is an example of the ~/Library/LaunchAgents/com.company.AppName.LaunchAgent.plist:
<?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>AssociatedBundleIdentifiers</key>
<string>com.company.AppName</string>
<key>KeepAlive</key>
<false/>
<key>Label</key>
<string>com.company.AppName.LaunchAgent</string>
<key>ProgramArguments</key>
<array>
<string>/Users/username/Library/Application Support/com.company.AppName/Launch Agent.app/Contents/MacOS/LaunchAgent</string>
</array>
<key>RunAtLoad</key>
<false/>
<key>WatchPaths</key>
<array>
<string>/Users/username/Applications</string>
<string>/Applications</string>
<string>/Network/Applications</string>
</array>
</dict>
</plist>
With the executable being a standard app bundle in /Users/username/Library/Application Support/com.company.AppName/Launch Agent.app
Thank you
I have created an OpenDirectory module based on the template and docs here: https://developer.apple.com/library/archive/releasenotes/NetworkingInternetWeb/RN_OpenDirectory/chapters/chapter-1.xhtml.html
After I copy my module in place and I set my module's configuration (see Configuration APIs section), my module does not get loaded. Currently the way I am able to start/reload it is sending a TERM signal to "opendirectoryd". (Launchctl refuses to stop it.) Then launchd restarts it, and my module gets started fine. Problem is that on some macOS this leads to system inresponsiveness for long time (even minutes).
I have tried HUP signal, odutil reset cache etc, they do not help, my module does not get recognized.
Is there a recommended way how to notify opendirectoryd about a new module?
Repro: My example module can be found here: https://www.dropbox.com/scl/fi/qb8pa100yy56n5hangad0/MyODModule-250527-131702.tar.gz?rlkey=m96vb1rrxc6hml878jn64ybc8&st=h22tl4cy&dl=0
To reproduce the behaviour, uncomment line 12 in register_odmodule.sh: "/usr/bin/killall opendirectoryd", and compile and install the module with
"make && sudo make install". And observe that it does not get loaded. Then "killall opendirectoryd", and observe that it got loaded.
(To test for loaded or not, you can read on the node it creates with dscl: "dscl /MyExample -list /", or just see that it is not started as a process with "ps").
Thanks for any help in advance!
I have an app which contains a bundled launch agent that I register using SMAppService.agent(plistName:). I’ve packaged the launch agent executable in the typical Mac app bundle structure so I can embed a framework in it. So, the launch agent lives in Contents/SharedSupport/MyLaunchAgent.app/Contents/MacOS/MyLaunchAgent.
However, I suspect this approach might be falling afoul of the scheduler, since the taskinfo tool reports my launch agent has a requested & effective role of TASK_DEFAULT_APPLICATION (PRIO_DARWIN_ROLE_UI), rather than the TASK_UNSPECIFIED (PRIO_DARWIN_ROLE_DEFAULT) value I see with system daemons.
I tried setting the LSUIElement Info.plist key of my launch agent to YES, but this seems to have had no effect.
What’s the recommended approach here?
I've searched around the internet and could not find a clear answer.
I have a swift command line tool that needs to run automatically when the Mac mini M4 is started up without a user login and continue running forever. However, the command line tool and the data it uses are located on an external disk due to the size of the data.
The service specified by a launchd plist located in /Library/LaunchDaemons tries to start up but fails because it cannot immediately find the command line tool. Which is because the external disk is not mounted when launchd tries to start the service when the Mac is booting. The service runs fine when bootstrapped after the disk is mounted.
The first error is "No such file or directory, error 0x6f - Invalid or missing Program/ProgramArguments" and the service is put in the "penalty box".
Is there any way for the service to get out of the "penalty box"?
What is the best approach to make the launchd service wait for a specific external disk to mount?
Some options for waiting seem to be:
Use "WatchPaths" in the launchd plist, but the man page says this is unreliable. This makes one wonder what is the purpose of this option?
Use "StartOnMount in the launchd plist", but this will run the command line tool every time any disk is mounted. This is not desired.
Of course, I could move the command line tool to the startup disk, but then the tool would fail because the data is not available. This could be remedied by modifying the command line tool to wait for the external disk, but it would be polling, which seems inefficient. I could also add a delay, but that seems error prone because there is no assurance that the delay is long enough.
When looking at the system plists, there seem to be a lot of options that are not directly mentioned in the man page for launchd.plist and have little to no documentation that I could find. Maybe there is something I am missing here?
In the end, I would just like to make sure the launchd service waits for the specific disk to be available before starting the service. Any ideas how best to do that?
Hi all,
I'm working on a non-interactive macOS application (a service or daemon), and I'm trying to understand the best practices around logging and error reporting, particularly in failure scenarios.
If a daemon or service fails in macOS, where is it expected to log errors, and how can users or developers discover what went wrong?
Specifically, I have a few questions:
What is the recommended location or system for logging errors from a non-interactive macOS application?
Should we use os_log, standard error output, or write directly to files somewhere?
How can a user or developer access these logs to diagnose issues—should logs be visible via the Console app?
Is there a standard approach to making failure information easily accessible for debugging and support, especially for daemons running under launchd?
Any guidance or best practices would be appreciated.
Say I want to sync a toggle in my app with SMAppService's .status property.
If the status changes from my app I can track it. But if user toggles it from System Settings, I don't see a notification so then the UI in my app is out of date.
The status property is not key value observable and there doesn't appear to be a SMAppServiceStatusDidChangeNotification ?
I can re-read it every time my app will become active but feels kind of wrong to do it this way.
For this code:
let status = try await container.accountStatus()
Seeing this error:
2025-05-08 15:32:00.945731-0500 localhost myAgent[2661]: (myDaemon.debug.dylib) [com.myDaemon.cli:networking] Error Domain=CKErrorDomain Code=6 "Error connecting to CloudKit daemon. This could happen for many reasons, for example a daemon exit, a device reboot, a race with the connection inactivity monitor, invalid entitlements, and more. Check the logs around this time to investigate the cause of this error." UserInfo={NSLocalizedDescription=Error connecting to CloudKit daemon. This could happen for many reasons, for example a daemon exit, a device reboot, a race with the connection inactivity monitor, invalid entitlements, and more. Check the logs around this time to investigate the cause of this error., CKRetryAfter=5, CKErrorDescription=Error connecting to CloudKit daemon. This could happen for many reasons, for example a daemon exit, a device reboot, a race with the connection inactivity monitor, invalid entitlements, and more. Check the logs around this time to investigate the cause of this error., NSUnderlyingError=0x600001bfc270 {Error Domain=NSCocoaErrorDomain Code=4099 UserInfo={NSDebugDescription=
I initially started the this process as System Daemon to see what would happen (which obviously does not have CloudKit features). Then moved it back to /Library/LaunchAgents/ and can't get rid of that error.
I see also following message from CloudKit daemon:
Ignoring failed attempt to get container proxy for <private>: Error Domain=NSCocoaErrorDomain Code=4099 UserInfo={NSDebugDescription=<private>}
Automatically retrying getting container proxy due to error for <private>: Error Domain=NSCocoaErrorDomain Code=4099 UserInfo={NSDebugDescription=<private>}
XPC connection interrupted for <private>
And this error for xpc service:
[0x130e074b0] failed to do a bootstrap look-up: xpc_error=[3: No such process]
If I start the same cli process directly from XCode, then it works just fine.
I have been playing with application bundled LaunchAgents:
I downloaded Apple sample code,
Run the sample code as is,
Tweaked the sample code a lot and changed the LaunchAgents IDs and Mach ports IDs,
Created new projects with the learnings, etc.
After deleting all the Xcode projects and related project products and rebooting my machine several times, I noticed the LaunchAgent are still hanging around in launchctl. If I write launchctl print-disabled gui/$UID (or user/$UID) I can see all my testing service-ids:
disabled services = {
"com.xpc.example.agent" => disabled
"io.dehesa.apple.app.agent" => disabled
"io.dehesa.sample.app.agent" => disabled
"io.dehesa.example.agent" => disabled
"io.dehesa.swift.xpc.updater" => disabled
"io.dehesa.swift.agent" => disabled
}
(there are more service-ids in that list, but I removed them for brevity purposes).
I can enable or disable them with launchctl enable/disable service-target, but I cannot really do anything else because their app bundle and therefore PLIST definition are not there anymore. How can I completely remove them from my system?
More worryingly, I noticed that if I try to create new projects with bundled LaunchAgents and try to reuse one of those service-ids, then the LaunchAgent will refuse to run (when it was running ok previously). The calls to SMAppService APIs such .agent(plistName:) and register() would work, though.
I am building an app that uses the SMAppService to register a LaunchDaemon that is bundled with my .app. I've got a priming flow created which walks the user through approving the service so that it will start on login.
However, I need to also be able to upgrade this background service if the user updates the app. To do this, I think I need to call unregisterAndReturnError and then registerAndReturnError.
From my testing, this seems to work correctly, but I have a concern. Will the user ever be prompted to re-authorize the LaunchDaemon that I am registering? If so, under what circumstances will that happen, and what does it look like (so that I can guide the user through it)?
I am encountering an issue with my application, BloxOneEndpoint.pkg, which includes two services:
rc_service_infoblox – Runs as the root user.
Controller Application – Runs as a normal user.
Although a thread within rc_service_infoblox is running fine and performing its expected tasks, I notice that the service appears as "Not Responding" in Activity Monitor. Despite normal functionality, this status is concerning, as it may indicate some issue to customer.
I would appreciate any insights into why this might be happening and how to resolve it. Is there a specific API or mechanism I should use to ensure the service remains in a "Running" state in Activity Monitor?
Thank you for your guidance.
Topic:
App & System Services
SubTopic:
Processes & Concurrency
Tags:
Endpoint Security
Service Management
Hi,
our CourAudio server plugin utilizes the SystemConfiguration.framework to store and restore specific shared system wide settings.
While our application can authenticate to utilize the SystemConfiguration.framework to gain write access to the shared configuration settings the CoreAudio server plugin obviously can't have any user interaction and therefor does not authenticate.
Is it possible to authenticate the CoreAudio server plugin to gain write permissions? Are there any entitlements or other means that would allow this?
Thanks!
Topic:
Media Technologies
SubTopic:
Audio
Tags:
System Configuration
Core Audio
Inter-process communication
Service Management
Hello.
Background: Most learning resources are for leaning Swift/Objective-C. I'm pretty sure I need something different. I'm already an experienced software engineer, just new to iOS/MacOS development. My problem is not learning the language, but rather how to learn modern best practices. I cannot find examples for what I'm looking for. So much seems to be sparse on implementation details, out of date, or both.
I'm trying to write an app that has a few distinct parts. The UI portion will be mostly a menu bar app, which I am not having a problem discovering resources for how to implement. The app will also have a daemon and utilize network extensions. This is where I am having trouble.
What's the current best practices on how to write and launch a daemon?
Should the daemon be its own library/package which is them imported into the main app? If so, which Xcode template do I use for this? Are there any Hello World! examples of this?
What is the best way for a UI app to communicate with a daemon?
Are there any Hello World! repositories on how to implement network extensions? Should this be done in the main UI app, or in a separate library/package?
TIA
Topic:
App & System Services
SubTopic:
General
Tags:
Network Extension
Service Management
Background Tasks
Hello,
I have a question about a edge case scenario.
Before that some info on my project-
I have a launchdaemon that carries out some business logic, it also has XPC listener (built using C APIs).
Question-
Can there be a situation when the daemon is up and running but the XPC listener is down(due to some error or crash)? If yes then do I need to handle it in my code or launchd will handle it?
when the daemon is stopped or shut down, how do I stop the XPC listener? After getting listener object from xpc_connection_create_mach_service should I just call xpc_connection_cancel followed by a call to xpc_release?
Thanks!
K
I have followed these steps as mentioned in this link :(https://developer.apple.com/forums/thread/721737)
My projects app bundle structure is like this :
TWGUI.app
TWGUI.app/Contents
TWGUI.app/Contents/_CodeSignature
TWGUI.app/Contents/_CodeSignature/CodeResources
TWGUI.app/Contents/MacOS
TWGUI.app/Contents/MacOS/TWAgent
TWGUI.app/Contents/MacOS/TWGUI
TWGUI.app/Contents/Resources
TWGUI.app/Contents/Library
TWGUI.app/Contents/Library/LaunchAgents
TWGUI.app/Contents/Library/LaunchAgents/com.example.TWGUI.agent.plist
TWGUI.app/Contents/Info.plist
TWGUI.app/Contents/PkgInfo
TWGUI is my main GUI App , i which i want to embed TWAgent (a command line tool target) and register it using SMAppServices so that launchd can launch it.
In TWGUI, code for registering to launchd using SMAppServices is structure as follow :
import SwiftUI
import ServiceManagement
struct ContentView: View {
let agent = SMAppService.agent(plistName: "com.example.TWGUI.agent.plist")
var body: some View {
VStack {
Button("Register Agent") {
RegisterAgent ()
}
.padding()
Button("Unregister Agent") {
UnregisterAgent ()
}
.padding()
}
}
func RegisterAgent() {
DispatchQueue.global(qos: .background).async {
do {
print("Registering Agent. Status: \(agent.status.rawValue)")
try agent.register()
print("Agent registered")
} catch {
print("Failed to register agent: \(error)")
}
}
}
func UnregisterAgent() {
DispatchQueue.global(qos: .background).async {
do {
print("Unregistering Agent. Status: \(agent.status.rawValue)")
try agent.unregister()
print("Agent unregistered")
} catch {
print("Failed to unregister agent: \(error)")
}
}
}
}
com.example.TWGUI.agent.plist :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs$
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.TWGUI.agent</string>
<key>ProgramArguments</key>
<array>
<string>Contents/MacOS/TWAgent</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
I have used ProgramArguements instead of using Program in above plist because i was getting this error when i was using Program earlier :
Registering Agent. Status: 3
Failed to register agent: Error Domain=SMAppServiceErrorDomain Code=111 "Invalid or missing Program/ProgramArguments" UserInfo={NSLocalizedFailureReason=Invalid or missing Program/ProgramArguments}
TWGUI apps Info.plist is :
<?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>BuildMachineOSBuild</key>
<string>23C71</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>TWGUI</string>
<key>CFBundleIdentifier</key>
<string>com.example.TWAgent</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>TWGUI</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string></string>
<key>DTPlatformName</key>
<string>macosx</string>
<key>DTPlatformVersion</key>
<string>14.2</string>
<key>DTSDKBuild</key>
<string>23C53</string>
<key>DTSDKName</key>
<string>macosx14.2</string>
<key>DTXcode</key>
<string>1510</string>
<key>DTXcodeBuild</key>
<string>15C65</string>
<key>LSMinimumSystemVersion</key>
<string>14.2</string>
</dict>
</plist>
TWAgent target has main.swift file which does this :
import Foundation
let startTime = CFAbsoluteTimeGetCurrent()
func logTimeSinceStart() {
let elapsedTime = CFAbsoluteTimeGetCurrent() - startTime
NSLog("Time since program started: \(elapsedTime) seconds")
}
func startLoggingTime() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
logTimeSinceStart()
}
}
// Start logging time
startLoggingTime()
// Keep the run loop running
CFRunLoopRun()
I followed these exact same steps in another project earlier and my agent was getting registered, although i lost that project due to some reasons.
But now i am getting this error when i am registering or unregistering agent using SMAppServices from the code above :
Registering Agent. Status: 3
Failed to register agent: Error Domain=SMAppServiceErrorDomain Code=1 "Operation not permitted" UserInfo={NSLocalizedFailureReason=Operation not permitted}
I tried diffrent fixes for like this :
Moved app bundle to /applications folder
Gave permission for full disc access to this app .
Code sign again (both agent and TWGUI
...
But nothing seems to work , getting same error.
I tried to launch agent using :
Launchctl load com.example.TWGUI.agent.plist
and it worked , so there is no issue with my plist implementation.
Can someone help me understand how can i solve this issue ? or if i am following right steps ? Can give steps need to follow to implement this and steps so that i can register and start my agent using SMAppServices?
And i also tried the project give in apples official documentation : [https://developer.apple.com/documentation/servicemanagement/updating-your-app-package-installer-to-use-the-new-service-management-api)
but got same error in this project as well .
Can I launch all the launchAgents associated with a user before they log in? If so, is there an event or mechanism I can use or simulate to achieve this?
Is it possible to delay the login mechanism, till all the launchAgents are loaded.
Looking for guidance on how to approach this.
I'm a developer using Lazarus Pascal, so converting ObjC and Swift comes with its challenges.
I'm trying to figure how to properly use SMAppService to add my application as a login item for the App Store.
I have learned that the old method (< macOS 13) uses a helper tool, included in the app bundle, which calls the now deprecated SMLoginItemSetEnabled. Now this is already quite a pain to deal with if you're not using XCode, not to mention converting the headers being rather complicated when you're not experienced with doing this.
The "new" method (as of macOS 13) is using SMAppService.
Can anyone explain how to use this? The documentation (for me anyway) is a not very clear about that and neither are examples that can be found all over the Internet.
My main question is:
Can I now use the SMAppService functions to add/remove a login item straight in my application, or is a helper tool still required?
This week I’m handling a DTS incident from a developer who wants to escalate privileges in their app. This is a tricky problem. Over the years I’ve explained aspects of this both here on DevForums and in numerous DTS incidents. Rather than do that again, I figured I’d collect my thoughts into one place and share them here.
If you have questions or comments, please start a new thread with an appropriate tag (Service Management or XPC are the most likely candidates here) in the App & System Services > Core OS topic area.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
BSD Privilege Escalation on macOS
macOS has multiple privilege models. Some of these were inherited from its ancestor platforms. For example, Mach messages has a capability-based privilege model. Others were introduced by Apple to address specific user scenarios. For example, macOS 10.14 and later have mandatory access control (MAC), as discussed in On File System Permissions.
One of the most important privilege models is the one inherited from BSD. This is the classic users and groups model. Many subsystems within macOS, especially those with a BSD heritage, use this model. For example, a packet tracing tool must open a BPF device, /dev/bpf*, and that requires root privileges. Specifically, the process that calls open must have an effective user ID of 0, that is, the root user. That process is said to be running as root, and escalating BSD privileges is the act of getting code to run as root.
IMPORTANT Escalating privileges does not bypass all privilege restrictions. For example, MAC applies to all processes, including those running as root. Indeed, running as root can make things harder because TCC will not display UI when a launchd daemon trips over a MAC restriction.
Escalating privileges on macOS is not straightforward. There are many different ways to do this, each with its own pros and cons. The best approach depends on your specific circumstances.
Note If you find operations where a root privilege restriction doesn’t make sense, feel free to file a bug requesting that it be lifted. This is not without precedent. For example, in macOS 10.2 (yes, back in 2002!) we made it possible to implement ICMP (ping) without root privileges. And in macOS 10.14 we removed the restriction on binding to low-number ports (r. 17427890). Nice!
Decide on One-Shot vs Ongoing Privileges
To start, decide whether you want one-shot or ongoing privileges. For one-shot privileges, the user authorises the operation, you perform it, and that’s that. For example, if you’re creating an un-installer for your product, one-shot privileges make sense because, once it’s done, your code is no longer present on the user’s system.
In contrast, for ongoing privileges the user authorises the installation of a launchd daemon. This code always runs as root and thus can perform privileged operations at any time.
Folks often ask for one-shot privileges but really need ongoing privileges. A classic example of this is a custom installer. In many cases installation isn’t a one-shot operation. Rather, the installer includes a software update mechanism that needs ongoing privileges. If that’s the case, there’s no point dealing with one-shot privileges at all. Just get ongoing privileges and treat your initial operation as a special case within that.
Keep in mind that you can convert one-shot privileges to ongoing privileges by installing a launchd daemon.
Just Because You Can, Doesn’t Mean You Should
Ongoing privileges represent an obvious security risk. Your daemon can perform an operation, but how does it know whether it should perform that operation?
There are two common ways to authorise operations:
Authorise the user
Authorise the client
To authorise the user, use Authorization Services. For a specific example of this, look at the EvenBetterAuthorizationSample sample code.
Note This sample hasn’t been updated in a while (sorry!) and it’s ironic that one of the things it demonstrates, opening a low-number port, no longer requires root privileges. However, the core concepts demonstrated by the sample are still valid.
The packet trace example from above is a situation where authorising the user with Authorization Services makes perfect sense. By default you might want your privileged helper tool to allow any user to run a packet trace. However, your code might be running on a Mac in a managed environment, where the site admin wants to restrict this to just admin users, or just a specific group of users. A custom authorisation right gives the site admin the flexibility to configure authorisation exactly as they want.
Authorising the client is a relatively new idea. It assumes that some process is using XPC to request that the daemon perform a privileged operation. In that case, the daemon can use XPC facilities to ensure that only certain processes can make such a request.
Doing this securely is a challenge. For specific API advice, see this post.
WARNING This authorisation is based on the code signature of the process’s main executable. If the process loads plug-ins [1], the daemon can’t tell the difference between a request coming from the main executable and a request coming from a plug-in.
[1] I’m talking in-process plug-ins here. Plug-ins that run in their own process, such as those managed by ExtensionKit, aren’t a concern.
Choose an Approach
There are (at least) seven different ways to run with root privileges on macOS:
A setuid-root executable
The sudo command-line tool
The authopen command-line tool
AppleScript’s do shell script command, passing true to the administrator privileges parameter
The osascript command-line tool to run an AppleScript
The AuthorizationExecuteWithPrivileges routine, deprecated since macOS 10.7
The SMJobSubmit routine targeting the kSMDomainSystemLaunchd domain, deprecated since macOS 10.10
The SMJobBless routine, deprecated since macOS 13
An installer package (.pkg)
The SMAppService class, a much-needed enhancement to the Service Management framework introduced in macOS 13
Note There’s one additional approach: The privileged file operation feature in NSWorkspace. I’ve not listed it here because it doesn’t let you run arbitrary code with root privileges. It does, however, have one critical benefit: It’s supported in sandboxed apps. See this post for a bunch of hints and tips.
To choose between them:
Do not use a setuid-root executable. Ever. It’s that simple! Doing that is creating a security vulnerability looking for an attacker to exploit it.
If you’re working interactively on the command line, use sudo, authopen, and osascript as you see fit.
IMPORTANT These are not appropriate to use as API. Specifically, while it may be possible to invoke sudo programmatically under some circumstances, by the time you’re done you’ll have code that’s way more complicated than the alternatives.
If you’re building an ad hoc solution to distribute to a limited audience, and you need one-shot privileges, use either AuthorizationExecuteWithPrivileges or AppleScript.
While AuthorizationExecuteWithPrivileges still works, it’s been deprecated for many years. Do not use it in a widely distributed product.
The AppleScript approach works great from AppleScript, but you can also use it from a shell script, using osascript, and from native code, using NSAppleScript. See the code snippet later in this post.
If you need one-shot privileges in a widely distributed product, consider using SMJobSubmit. While this is officially deprecated, it’s used by the very popular Sparkle update framework, and thus it’s unlikely to break without warning.
If you only need escalated privileges to install your product, consider using an installer package. That’s by far the easiest solution to this problem.
Keep in mind that an installer package can install a launchd daemon and thereby gain ongoing privileges.
If you need ongoing privileges but don’t want to ship an installer package, use SMAppService. If you need to deploy to older systems, use SMJobBless.
For instructions on using SMAppService, see Updating helper executables from earlier versions of macOS.
For a comprehensive example of how to use SMJobBless, see the EvenBetterAuthorizationSample sample code. For the simplest possible example, see the SMJobBless sample code. That has a Python script to help you debug your setup. Unfortunately this hasn’t been updated in a while; see this thread for more.
Hints and Tips
I’m sure I’ll think of more of these as time goes by but, for the moment, let’s start with the big one…
Do not run GUI code as root. In some cases you can make this work but it’s not supported. Moreover, it’s not safe. The GUI frameworks are huge, and thus have a huge attack surface. If you run GUI code as root, you are opening yourself up to security vulnerabilities.
Appendix: Running an AppleScript from Native Code
Below is an example of running a shell script with elevated privileges using NSAppleScript.
WARNING This is not meant to be the final word in privilege escalation. Before using this, work through the steps above to see if it’s the right option for you.
Hint It probably isn’t!
let url: URL = … file URL for the script to execute …
let script = NSAppleScript(source: """
on open (filePath)
if class of filePath is not text then
error "Expected a single file path argument."
end if
set shellScript to "exec " & quoted form of filePath
do shell script shellScript with administrator privileges
end open
""")!
// Create the Apple event.
let event = NSAppleEventDescriptor(
eventClass: AEEventClass(kCoreEventClass),
eventID: AEEventID(kAEOpenDocuments),
targetDescriptor: nil,
returnID: AEReturnID(kAutoGenerateReturnID),
transactionID: AETransactionID(kAnyTransactionID)
)
// Set up the direct object parameter to be a single string holding the
// path to our script.
let parameters = NSAppleEventDescriptor(string: url.path)
event.setDescriptor(parameters, forKeyword: AEKeyword(keyDirectObject))
// The `as NSAppleEventDescriptor?` is required due to a bug in the
// nullability annotation on this method’s result (r. 38702068).
var error: NSDictionary? = nil
guard let result = script.executeAppleEvent(event, error: &error) as NSAppleEventDescriptor? else {
let code = (error?[NSAppleScript.errorNumber] as? Int) ?? 1
let message = (error?[NSAppleScript.errorMessage] as? String) ?? "-"
throw NSError(domain: "ShellScript", code: code, userInfo: nil)
}
let scriptResult = result.stringValue ?? ""
Revision History
2025-03-24 Added info about authopen and osascript.
2024-11-15 Added info about SMJobSubmit. Made other minor editorial changes.
2024-07-29 Added a reference to the NSWorkspace privileged file operation feature. Made other minor editorial changes.
2022-06-22 First posted.