Webinar: Mac Software Management for MSPs: From Visibility to Governance
Register now
Guide

How to run a Homebrew service as yourself

Petros Amoiridis

On a managed Device, brew services start runs a service as the workbrew account, with its home at /opt/workbrew/home. Most services work that way. Some need your own account instead, because they read files only you can open, keep their settings in your home folder, or draw on your screen. Colima ignoring ~/.colima, a menu bar app that starts but never appears, or Syncthing that can't read a folder you own are all signs of this.

This guide shows how to run such a service from a LaunchAgent of your own. The service still comes from Homebrew, so brew upgrade keeps updating it. What changes when brew runs as the workbrew account explains why brew services works this way.

Stop the service under brew services

If you already started the service with brew services, stop it so two copies don't run at once.

brew services stop <formula>

Find the command the service runs

Homebrew prints it in the formula's caveats, after "if you don't want/need a background service you can just run". To see the caveats again, run:

brew info <formula>

For Syncthing, the command is:

/opt/homebrew/opt/syncthing/bin/syncthing --no-browser --no-restart

Keep the /opt/homebrew/opt/<formula> path rather than a path with a version number in it. That path follows upgrades.

Write the LaunchAgent

Save a plist in ~/Library/LaunchAgents. Name it, and give it a label, that doesn't start with homebrew.mxcl. or sh.brew.. Workbrew stops and removes Homebrew's own service plists from users' LaunchAgents when it takes over Homebrew on a Device and when it updates, so a plist with one of those names won't last. local.<formula> works.

This example is ~/Library/LaunchAgents/local.syncthing.plist. Put the command from the previous step in ProgramArguments, one item per word.

<?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>local.syncthing</string>
  <key>ProgramArguments</key>
  <array>
    <string>/opt/homebrew/opt/syncthing/bin/syncthing</string>
    <string>--no-browser</string>
    <string>--no-restart</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
</dict>
</plist>

RunAtLoad starts it when you log in, and KeepAlive restarts it if it exits. Drop KeepAlive for a service that should run once and stop.

Load it

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.syncthing.plist

From then on it starts each time you log in. To check that it runs as you:

ps -axo user,command | grep syncthing

After an upgrade

brew upgrade replaces the files but leaves the running copy alone. Restart it to pick up the new version.

launchctl kickstart -k gui/$(id -u)/local.syncthing

Clean up the old copy

The copy that ran under brew services kept its settings and data under /opt/workbrew/home, separate from yours. Those files belong to the workbrew account, so removing them needs an administrator and sudo. Leaving them in place does no harm while the service is stopped.

To go back to brew services, unload your LaunchAgent and delete it first.

launchctl bootout gui/$(id -u)/local.syncthing
rm ~/Library/LaunchAgents/local.syncthing.plist