> For the complete documentation index, see [llms.txt](https://docs.stepsecurity.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.stepsecurity.io/developer-machines/installation/script/mdm-deployment/macos/launchd-troubleshooting.md).

# 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](/developer-machines/installation/script.md) 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`

```xml
<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:

```bash
plutil -p "$PLIST" | grep -A4 ProgramArguments
# /bin/bash .../stepsecurity-loader.sh send-telemetry   -> loader-managed (auto-updates each tick)
# .../stepsecurity-dev-machine-guard send-telemetry     -> binary-managed (no auto-update)
```

#### Shell setup for the commands below

```bash
LABEL=com.stepsecurity.agent

# Expected: per-user LaunchAgent
DOMAIN="gui/$(id -u)"
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
LOGDIR="$HOME/.stepsecurity"

# Check whether a root LaunchDaemon is also present (rare). If it is, redo with
# sudo and: DOMAIN=system  PLIST=/Library/LaunchDaemons/$LABEL.plist  LOGDIR=/var/log/stepsecurity
ls -la "$HOME/Library/LaunchAgents/$LABEL.plist" 2>&1
ls -la "/Library/LaunchDaemons/$LABEL.plist" 2>&1
```

#### Check status

```bash
launchctl list | grep stepsec                          # loaded? PID + last exit code
launchctl list "$LABEL"                                # one-job summary
launchctl print "$DOMAIN/$LABEL"                       # full state, schedule, last exit
launchctl print-disabled "$DOMAIN" | grep stepsec      # disabled override? (loads but never runs)
launchctl enable "$DOMAIN/$LABEL"                      # clear a disable override
```

#### Inspect the plist

```bash
plutil -p "$PLIST"                                     # readable dump
plutil -lint "$PLIST"                                  # validate XML
plutil -p "$PLIST" | grep -A4 ProgramArguments         # loader script vs. binary
/usr/libexec/PlistBuddy -c "Print :StartInterval" "$PLIST"          # seconds (14400 = 4 hours)
/usr/libexec/PlistBuddy -c "Print :EnvironmentVariables" "$PLIST"   # baked HOME / STEPSECURITY_HOME
```

#### Check config and version

```bash
cat "$HOME/.stepsecurity/config.json"                  # effective config (contains api_key)
cat "$HOME/.stepsecurity/.current_version"             # version the loader last installed
"$HOME/.stepsecurity/bin/stepsecurity-dev-machine-guard" --version   # running binary version
ls -la "$HOME/.stepsecurity" "$HOME/.stepsecurity/bin" # owner should be the console user, not root
```

#### Read the logs

```bash
tail -n 100 "$LOGDIR/agent.log"                        # scheduled-run stdout
tail -n 100 "$LOGDIR/agent.error.log"                  # scheduled-run stderr (rotates to .prev at 5 MiB)
tail -f "$LOGDIR"/agent.log "$LOGDIR"/agent.error.log  # watch live
tail -n 50 "$HOME/.stepsecurity/ai-agent-hook-errors.jsonl"   # AI agent hook errors
stat -f '%Sm' "$LOGDIR/agent.log"                      # last scheduled-run time
log show --predicate 'process == "launchd"' --last 2h | grep -i stepsec   # launchd's own view
```

#### Force a run

```bash
launchctl kickstart -k "$DOMAIN/$LABEL"                # run now (-k restarts if a run is in flight)
/bin/bash "$HOME/.stepsecurity/bin/stepsecurity-loader.sh" send-telemetry   # run the loader by hand (update + scan)
```

#### Reload after editing the plist

```bash
launchctl bootout   "$DOMAIN/$LABEL" 2>/dev/null
launchctl bootstrap "$DOMAIN" "$PLIST"
launchctl print     "$DOMAIN/$LABEL" | head -20
```

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

#### Uninstall

```bash
/bin/bash "$HOME/.stepsecurity/bin/stepsecurity-loader.sh" uninstall   # loader-managed (MDM)
"$HOME/.stepsecurity/bin/stepsecurity-dev-machine-guard" uninstall     # binary-managed

# Manual fallback:
launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || launchctl unload "$PLIST" 2>/dev/null
rm -f "$PLIST"

# Verify
launchctl list | grep stepsec                          # expect no output
ls -la "$PLIST" 2>&1                                   # expect not found
rm -rf "$HOME/.stepsecurity"                           # wipe local state (optional)
```

#### Reinstall

```bash
/bin/bash "$HOME/.stepsecurity/bin/stepsecurity-loader.sh" install   # or re-push the loader via MDM
launchctl print "$DOMAIN/$LABEL" | grep -iE 'state|last exit'
launchctl kickstart -k "$DOMAIN/$LABEL" && tail -n 20 "$LOGDIR/agent.log"
```

#### 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](/developer-machines/installation/script/mdm-deployment/macos.md#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](/developer-machines/installation/script/mdm-deployment/macos.md#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`.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.stepsecurity.io/developer-machines/installation/script/mdm-deployment/macos/launchd-troubleshooting.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
