The LaunchAgent/LaunchDaemon Split in brew services: Avoiding Permission Drift
Homebrew formulas install either LaunchAgents or LaunchDaemons. Learn how to check which one a formula uses before running brew services, and why starting a system daemon without sudo—or with unnecessary sudo—creates permission problems.
15 Apr 2026, 22:38 UTC

You install a local database or cache with Homebrew, run brew services start, and one of two things happens: the command fails with a permissions error, or it succeeds but leaves root-owned files in a directory your normal user can no longer write to. The useful takeaway is that brew services does not choose whether a service runs as your user or as root—the formula does. Your job is to check that boundary before you type sudo.
How brew services maps to launchd domains
On macOS, background processes are managed by launchd. When a Homebrew formula ships a service definition, it places a property list (plist) in either Library/LaunchAgents or Library/LaunchDaemons. The brew services subcommand reads those files and calls launchctl on your behalf. It has been built into Homebrew since version 2.0 and requires no extra dependencies.
The directory matters. LaunchAgents run in your user session, inheriting your environment and file permissions. LaunchDaemons run at the system level, typically as root. brew services preserves that boundary. It does not silently promote a user agent to a system daemon, nor does it demote a daemon to run as you.
Checking the domain before you start
Suppose you want a local PostgreSQL instance. Run the following in your macOS Terminal as your normal user:
brew install postgresql@16
After installation, confirm Homebrew recognizes the service:
brew services list
The formula should appear with a status such as stopped or none. That listing confirms a plist exists, but it does not always tell you which launchd domain the formula targets. To find that, inspect the formula's metadata:
brew info postgresql@16
Look for a plist path in the output. If the path contains LaunchAgents, the service is designed to run as your user. If it contains LaunchDaemons, it is a system-wide service. If the output is ambiguous, consult the formula's documentation or inspect the plist directly under the formula's prefix.
A practical first attempt is to start the service without elevated privileges:
brew services start postgresql@16
If the formula ships a LaunchAgent, this should succeed. If it ships a LaunchDaemon, the command may fail or request authentication. Do not reflexively retry with sudo; confirm the formula is intended as a system daemon first.
The cost of unnecessary sudo
Starting a system daemon with sudo brew services start runs the process as root. Any files the service creates—data directories, logs, Unix sockets—are owned by root. Later, when you try to read logs or run maintenance commands as your normal user, you may hit permission errors. This is permission drift: the service state no longer matches your user's expectations.
For example, if a root-owned PostgreSQL daemon initializes a data directory under /usr/local/var, your unprivileged psql client may be unable to write there. The fix usually involves chown or sudo for routine tasks, which erodes the convenience Homebrew is supposed to provide.
The reverse mistake is also possible. If a formula only ships a LaunchDaemon but you need it only for local development, running it as root is overkill. In that case, consider running the binary directly in your shell or managing a user-level plist yourself, accepting that brew services will not control it.
A practical decision framework
- Development services (databases, caches, message queues): prefer formulas that install
LaunchAgents. Start them normally and keep file ownership aligned with your user. - System-wide services (network daemons, system utilities): if the formula ships a
LaunchDaemon, usesudoonly when you genuinely need system-level availability, and plan for root-owned state. - After starting, check ownership of the service's data directory with
ls -lato confirm it matches your expectations. If you seerootwhere you expected your username, stop the service and reconsider the approach.
Limitations
brew services only manages formulas that ship a service plist. Custom launchd jobs or daemons installed outside Homebrew must be managed with launchctl directly. Additionally, brew services is a convenience layer, not a process supervisor. It does not restart a crashed service unless the underlying plist defines KeepAlive, and it does not abstract away the launchd domain.
Because launchctl behavior has evolved across macOS releases, the exact commands brew services issues under the hood can differ by system version. If you need precise control over boot-time behavior or run-level domains, inspect the plist and use launchctl directly.
Actionable check
Before your next brew services start, run brew services list to confirm the formula is recognized, then use brew info to identify the plist domain. Start without sudo first. If the command fails, verify the formula is a system daemon before escalating. Finally, run ls -la on the service's data directory to ensure file ownership is what you intended.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.