Skip to content

Per-user LaunchAgent

This guide is for MDM admins who want App Auto-Patch (AAP) to run continuously in every user’s session — auto-launching at login, restarting on unexpected exits, and surfacing pending updates via the menu bar item + user notification without ever popping the dashboard window in the user’s face. It’s the right deployment shape for managed fleets and is what TechTrekkie originally asked for in issue #33.

Deploy three things in this order:

  1. The AAP .pkg (any current build, via Jamf / Munki / Intune / etc.).
  2. A configuration profile with the AAP settings you want centrally managed (e.g. IgnoredLabels, deferral deadlines, support-team info — see Profile Manifests/README.md for the schema). As of build 40 you do NOT need to explicitly set ManagedSilentLaunchEnabled = true — the moment any AAP-aware managed preference is present, silent launch flips on by default. If you want managed Macs to keep the dashboard visible at launch, deploy ManagedSilentLaunchEnabled = false explicitly.
  3. The LaunchAgent plist in this folder (xyz.techitout.appAutoPatch.agent.example.plist) dropped at /Library/LaunchAgents/xyz.techitout.appAutoPatch.agent.plist, owner root:wheel, mode 0644. After the next user login, AAP launches silently, runs discovery + update check, and surfaces a user notification when pending updates are found. The user can open the dashboard from the menu bar item (Open App Auto-Patch) or by double-clicking the .app from Finder / Launchpad / Dock — both paths work normally; the silent-launch suppression only applies to the auto-opened window at process start.

This was the cause of the “Setup Required” state TechTrekkie reported when he tried to keep AAP running via a system LaunchDaemon with UserName = root. App Auto-Patch has two cooperating process types, and they have different lifecycle owners:

  • The root helper LaunchDaemon (xyz.techitout.appAutoPatch.roothelper) is what runs Installomator, installs packages, and writes receipts. It’s already deployed by AAP at install time via Apple’s SMAppService API and runs as root. You don’t manage this one — AAP does.
  • The AAP GUI (App Auto-Patch.app) renders the dashboard, menu bar item, notifications, and Settings. It must run as the logged-in user, not as root, because:
    1. SMAppService helper approval is a per-user record. A root-launched GUI has no access to the user’s approval status, so HelperInstallationService.isOperational returns false forever and the dashboard surfaces Setup Required — exactly what TechTrekkie’s screenshot showed.
    2. The GUI needs an attached Aqua window server to render the menu bar item and post user notifications. The root context under launchd’s system domain has no window server.
    3. AAP’s UI-side state (DiscoveredAppsData, FirstRunSetupCompleted, LastNotifiedPendingUpdatesCount, etc.) lives in ~/Library/Preferences/. Running as root would persist these into /var/root/Library/Preferences/, where the real user can never see them. A LaunchAgent runs once per logged-in user, as that user, in their Aqua session — which is exactly the right context for the AAP GUI. Build 39 also logs a prominent [WARNING] line at startup when getuid() == 0, so future LaunchDaemon-as-root deployments show up in the diagnostic bundle.

Use the sample at Profile Manifests/xyz.techitout.appAutoPatch.agent.example.plist — copy it to /Library/LaunchAgents/xyz.techitout.appAutoPatch.agent.plist on every managed Mac. Key fields:

  • Label: xyz.techitout.appAutoPatch.agent
  • ProgramArguments: /Applications/App Auto-Patch.app/Contents/MacOS/App Auto-Patch
  • RunAtLoad: true
  • KeepAlive → SuccessfulExit: false (re-spawn on crash, respect Cmd+Q from the user)
  • LimitLoadToSessionType: Aqua
  • ThrottleInterval: 30 (cap the relaunch rate so a wedged build can’t spin)
  • StandardOutPath / StandardErrorPath: redirected into /Library/Application Support/AppAutoPatch/logs/ for early-launch crash capture.
  • EnvironmentVariables → AAP_LAUNCH_AGENT: 1. Build 89 allows one GUI per user (#203). This variable tells that process it belongs to the LaunchAgent, so it replaces any copy open or Finder already started. A plist deployed before build 89 needs this key added. Without it, a second launch still exits, and the copy that started first is the one that stays.

Build 89 can write the login agent itself. The About tab has Open at login and Keep running if quit. A configuration profile can set OpenAtLogin and KeepAlive (both default false). Each toggle is locked when that key is managed, and both are locked when LockSettings is true.

KeepAlive starts App Auto-Patch again after the user quits it, and also starts it at login. OpenAtLogin alone starts it at login and still lets a quit stay quit.

The app writes ~/Library/LaunchAgents/xyz.techitout.appAutoPatch.login.plist. If /Library/LaunchAgents/xyz.techitout.appAutoPatch.agent.plist is already installed, the app does not write that second agent, and the Settings toggles stay off. Deploy one or the other, not both.

The agent plist must be owned by root:wheel with mode 0644. macOS refuses to load LaunchAgents with looser permissions.

  1. Build a .pkg that drops the file at /Library/LaunchAgents/xyz.techitout.appAutoPatch.agent.plist with the right ownership and mode (e.g. via pkgbuild --root from a prepared payload directory).
  2. Sign the package with your developer certificate.
  3. Upload to Jamf and scope to the managed-Mac smart group.
  4. On the next login (or sudo launchctl bootstrap gui/<uid> /Library/LaunchAgents/xyz.techitout.appAutoPatch.agent.plist for an active session) the agent loads.
  1. Build the same .pkg and treat it as a managed_installs item.
  2. Add a requires dependency on the AAP app package so the agent is never installed without the binary it points to.
  1. Package the LaunchAgent plist into a .pkg (Intune accepts shell scripts too; either works).
  2. Assign to the managed user/device group.

After the next user login, on a managed Mac:

Terminal window
sudo launchctl print gui/$(stat -f %u /dev/console)/xyz.techitout.appAutoPatch.agent

You should see the job listed, state running, and the path matching the installed bundle. aap.log will contain a fresh AppAutoPatch: Launched v4.0.0 (39); ... silentLaunch=true; uid=<user uid> line — the uid field is what proves the GUI is running as the logged-in user rather than as root.

The LaunchAgent on its own would auto-launch AAP at login with a visible Dock icon and dashboard window — useful for consumer Macs, but not what managed fleets want. Build 40 makes silent launch the managed-Mac default: if your config profile sets any AAP-aware managed preference (e.g. IgnoredLabels, DeferralTimerDefault, SupportTeamWebsite), AAP automatically launches in .accessory activation policy and auto-closes its initial dashboard window. No extra opt-in key is required. From the user’s point of view AAP is invisible until either:

  • a pending update is found and UpdatesAvailableNotifier posts a user notification, or
  • they click the menu bar item. Both surfaces are unaffected by silent-launch mode. If you specifically want managed Macs to keep the dashboard visible on launch (e.g. for kiosk-style deployments), set ManagedSilentLaunchEnabled = false in the profile. Explicit values always win over the auto-default.

Migrating from a LaunchDaemon-as-root deployment

Section titled “Migrating from a LaunchDaemon-as-root deployment”

If you previously deployed AAP via a system LaunchDaemon with UserName = root (the original TechTrekkie configuration), do this on the affected Macs:

Terminal window
# Stop and unload the broken root daemon
sudo launchctl bootout system /Library/LaunchDaemons/xyz.zillow.appautopatch.plist
sudo rm /Library/LaunchDaemons/xyz.zillow.appautopatch.plist
# Drop the new agent into place (assuming the file is already staged)
sudo install -o root -g wheel -m 0644 \
/path/to/xyz.techitout.appAutoPatch.agent.plist \
/Library/LaunchAgents/xyz.techitout.appAutoPatch.agent.plist
# Bootstrap for the currently-logged-in user (or wait for next login)
CONSOLE_USER_UID=$(stat -f %u /dev/console)
sudo launchctl bootstrap gui/${CONSOLE_USER_UID} \
/Library/LaunchAgents/xyz.techitout.appAutoPatch.agent.plist

After this the Setup Required state should clear (the helper was fine all along — only the GUI’s view of it was broken because it couldn’t read the user’s SMAppService approval record).

  • LimitLoadToSessionType = Aqua is required. Without it, launchd also tries to launch the agent in the LoginWindow and Background session types, which fail because the GUI requires an attached window server.
  • StandardOutPath / StandardErrorPath write to /Library/Application Support/AppAutoPatch/logs/. That directory is owned root:staff 0775 (per the build-32 #16 fix), so any local user can read and append. The actual AAP log (aap.log) remains the canonical source; the launchagent.stdout/stderr files are only relevant for crashes before LoggingService initializes.
  • Cmd+Q is honored. The menu-bar popover’s Quit App Auto-Patch exits the process cleanly, which is treated as a successful exit and does not cause launchd to re-spawn AAP. The user has to log out and back in (or run launchctl kickstart manually) to bring the agent back. This is intentional — when a user explicitly quits, we respect that.
  • The LaunchDaemon (root helper) is unaffected. It’s still managed by SMAppService, still runs as root, still does the privileged work. The agent only manages the GUI surface.
  • One GUI per user (build 89, #203). The app package’s postinstall used to open -gj the app even when this agent was also installed, which left two menu bar icons. From build 89, if this plist is already on disk, postinstall bootstraps and kickstarts the agent instead of opening a second copy. A second launch of the app exits. Redeploy the sample plist so it includes AAP_LAUNCH_AGENT=1; an agent that starts with that variable replaces the other copy. Quit any doubled processes once after upgrading. They do not merge on their own.
  • Settings can own the login agent instead. OpenAtLogin and KeepAlive write a different plist, xyz.techitout.appAutoPatch.login, in the user’s LaunchAgents folder. Leave this organization plist undeployed when you use those keys.
  • Profile Manifests/README.md — overview of managed-preference deployment (Jamf JSON schema vs. ProfileCreator/iMazing pfm format).
  • Profile Manifests/MDM-ServiceManagement.md — how to pre-approve the root helper LaunchDaemon via a com.apple.servicemanagement payload so users never see the requires approval state.
  • Profile Manifests/MDM-Notifications.md — how to pre-allow (or fleet-wide suppress) macOS user notifications for AAP so the silent-launch path can surface pending updates without a permission prompt.