For the complete documentation index, see llms.txt. This page is also available as Markdown.

launchd Troubleshooting

This page is a command reference for diagnosing the Dev Machine Guard launchd job on macOS (label com.stepsecurity.agent). It applies when the agent was deployed with the Scheduled via launchd pattern from the Script wizard. If your fleet uses the MDM-scheduled pattern, launchd is not involved: check your MDM's script execution logs instead.

How scheduled runs work

The loader installs a launchd plist with a StartInterval (default 4 hours). On each tick, launchd re-runs the loader script, which auto-updates the binary and then runs send-telemetry.

RunAtLoad is set to false, so loading the plist (at login, boot, or install) never triggers a scan. Only the interval does. The one-off initial scan runs explicitly at install time. To force an out-of-cycle run, use kickstart

<key>StartInterval</key>
<integer>14400</integer>   <!-- fire every 4 hours -->
<key>RunAtLoad</key>
<false/>                   <!-- do not run at load -->

RunAtLoad=false avoids a redundant scan on every login and reboot, and prevents a fleet-wide scan stampede at boot time. The practical consequence: after a bootstrap, load, or reload, nothing runs on its own. Use kickstart to trigger a scan immediately.

LaunchAgent vs. LaunchDaemon

The expected setup is almost always a per-user LaunchAgent running as the console user. That is what the loader installs. Even when your MDM runs the loader as root, it resolves the console user and installs a per-user LaunchAgent. If no one is logged in, it aborts with no_user rather than falling back to root.

A root LaunchDaemon under /Library/LaunchDaemons/ only appears in two cases: a legacy agent script installed as root before the loader architecture, or a manual sudo <binary> install. Current tooling does not create one, but check for a leftover when cleaning up.

Per-user LaunchAgent (expected)
Root LaunchDaemon (rare)

Plist

~/Library/LaunchAgents/com.stepsecurity.agent.plist

/Library/LaunchDaemons/com.stepsecurity.agent.plist

Domain

gui/$(id -u)

system

Runs as

console user

root

Logs

~/.stepsecurity/agent.log, agent.error.log

/var/log/stepsecurity/agent.log, agent.error.log

sudo

no

yes (use the system domain)

To tell a loader-managed install (MDM, auto-updates) apart from a binary-managed one (manual install, no auto-update), check what the plist runs:

Shell setup for the commands below

Check status

Inspect the plist

Check config and version

Read the logs

Force a run

Reload after editing the plist

Changes to config.json need no reload. The config is read at run time, so a kickstart is enough.

Uninstall

Reinstall

Common issues

  • config.json is rewritten every tick. The loader's write_config() keeps only a fixed set of fields (customer_id, api_endpoint, api_key, scan_frequency_hours, plus optional install_dir, max_execution_duration, and scan toggles). Any other hand-edited field, such as include_tcc_protected, is wiped within one interval. To make a field stick, edit the loader script's write_config() heredoc before deploying it. See macOS TCC permissions for the fleet rollout pattern.

  • Runs only in a live GUI session. With no console user (login window, headless, SSH), the LaunchAgent is not loaded and will not fire. The loader's initial run errors with no_user, and launchctl ... gui/<uid> over SSH can return Bootstrap failed: 5.

  • TCC prompts are real. The agent runs in the user's GUI session, so scanning ~/Documents, ~/Downloads, and similar paths pops permission dialogs. These paths are skipped by default. To scan them without prompts, grant Full Disk Access via a PPPC profile and set include_tcc_protected. See macOS TCC permissions .

  • A wedged run blocks every tick. The binary's lock file makes overlapping runs exit. A hung run holds the lock until the loader kills processes older than its maximum process age on a later tick. This self-heals, but scans are lost until it does.

  • StartInterval quirks. Fires missed during sleep coalesce into a single run on wake. The timer also restarts on each load and login, so short sessions on a long interval can starve the schedule.

  • Bootstrap failed: 5 most often means the job is already loaded. Run bootout first, then bootstrap.

Last updated

Was this helpful?