Skip to main content
Working with an agent? Give them a link to this page as markdown.

MDM rollout

Instead of every developer running the installer, an MDM can install and maintain the Watcher client across a fleet of managed machines. This page uses Kandji (now Iru) as the concrete example; the pattern carries over to other MDMs. For VMs built from a shared image, see baking the client into a VM image instead.

For downloadable audit, install, and uninstall scripts, see the Iru provider sample.

The audit-and-remediate pattern

Two scripts per machine, run by the MDM on a schedule (every 15 minutes to daily works well):

  • An audit script decides whether the machine needs work: the binary or its runtime tree is missing, the logged-in user's hooks or login agent are absent, or the daemon is not answering.

  • A remediation script installs Watcher when absent, repairs hooks and autostart, and restarts the daemon. Use the downloadable remediate-macos.sh sample and run it as the logged-in user.

    Set WATCHER_API_URL in the remediation script. The running client's built-in scheduler follows the server version, including downgrades during a rollback.

    When watcher doctor hooks reports drift, hooks install --yes repairs it and auto-approves the Codex config.toml changes that would prompt for consent on an interactive install (see supported agents). An MDM rollout therefore grants that consent fleet-wide by design; make sure that is a decision, not a surprise. Codex's own hook approval is separate and per user: it asks each user to confirm the changed hooks the next time it starts, and records nothing until they accept.

There is nothing to build, pack, or upload: the installer downloads the release, verifies its checksum, installs under the user's ~/.local, records the backend target beside the binary (the client reads it on every start and refuses to start without one), and registers the agent hooks. Because the install is per-user, the scripts must run as the logged-in user, not only as root; on multi-user machines, repeat per account. The installer handles only the first installation. Later remediation runs keep hooks and autostart healthy while the running client's built-in scheduler owns version changes.

Start on login

watcher autostart install writes the client's login agent (a macOS LaunchAgent or Linux systemd user unit). At login the agent runs a launcher, which exits quietly when the start-on-login toggle is off and starts the client otherwise. By default the binary is its own launcher (client 0.16.0 and later; older clients always require --launcher).

A wrapper script is passed --autostart, which is how it learns the launch came from the agent and can check the toggle before it execs anything. The binary is registered with no arguments, because it recognizes the agent by itself. That same recognition is why a wrapper you supply can end in a bare exec of the binary and still work against every client version, and the bare registration is why an agent one client installs stays runnable by another. Together they let your rollout and your client version move independently, in either order.

One caveat if you pin back to a client older than 0.16.0 without also reinstalling the login agent: that client does not check the start-on-login toggle when the agent runs the binary directly, so it starts the client at login even when the toggle is off. Reinstalling the agent, or upgrading again, restores it.

The login agent starts with no environment beyond PATH. If your deployment needs environment variables at launch, point --launcher at a small wrapper that exports them. Proxy authentication mode is the main case: the client only skips its SSO sign-in flow when WATCHER_AUTH_MODE=proxy is set at launch. A custom WATCHER_BACKEND_PORT works the same way.

#!/usr/bin/env bash
set -euo pipefail

# Environment the login agent needs: launchd provides none beyond PATH.
export WATCHER_AUTH_MODE=proxy

# Honor the start-on-login toggle when launchd starts us.
if [ "${1:-}" = "--autostart" ]; then
"$HOME/.local/bin/watcher" autostart enabled || exit 0
fi

exec "$HOME/.local/bin/watcher"

Register the agent as the logged-in user, not as root. MDM jobs run as root, and these commands write into the invoking user's home (the LaunchAgent plist, the settings toggle), so run each one through sudo -u <console-user> -H. Pass --launcher only when your deployment uses a wrapper:

user=<console-user>
bin="/Users/$user/.local/bin/watcher"
chown "$user" <path-to-wrapper>
chmod 0700 <path-to-wrapper>
sudo -u "$user" -H "$bin" autostart install --launcher <path-to-wrapper>
sudo -u "$user" -H "$bin" autostart enable
sudo -u "$user" -H "$bin" autostart start

A wrapper must be executable by the user launchd runs it as, and by no one else, hence user-owned 0700. It carries no secrets: proxy-header values are captured at install time (see below), not exported at every launch.

The three steps split cleanly across session boundaries: install only writes the plist into the user's ~/Library/LaunchAgents (launchd loads it at the next login, so it needs no access to the user's GUI session); enable turns the start-on-login toggle on in the user's settings; start is the only step that touches the live session, bootstrapping the agent into the user's GUI launchd domain so Watcher runs immediately instead of from the next login. If start fails because no GUI session is available, the agent still starts at the next login. All three are idempotent, so the remediation script can run them on every enforcement cycle.

Proxy-mode headers

On header-proxy deployments, WATCHER_HEADER_* variables must be present in the environment when the hooks are set up: when the installer runs, and on any later watcher hooks install (see sending additional headers). In the remediation script, set them inside the sudo -u invocation that installs or repairs hooks, for example:

sudo -u "$user" -H env \
WATCHER_BACKEND_PORT="$WATCHER_PORT" \
WATCHER_HEADER_X_FORWARDED_USER="$dev_email" \
"/Users/$user/.local/bin/watcher" hooks install --yes

Variables exported at the top of the root-level script do not reach the installer: sudo strips the calling environment by default. Setup captures the values into a per-deployment file that requests read, so the login agent and later launches need no header exports of their own, and a scheduled remediation re-run without the variables leaves the captured file untouched.

To rotate a header value, run watcher hooks install with the new values exported. To stop sending headers on a machine, first remove the WATCHER_HEADER_* variables from the environment, then run watcher hooks clear-headers as the user (the command refuses while the variables are still set).

Delivering managed settings

When your organization distributes managed client settings by MDM rather than via Watcher (the "Distribute via Watcher" toggle off), add a second audit-and-remediate pair that ships the saved YAML to each machine's ~/.apollo_monitor/settings.yaml. Write only that file: the developer's settings.local.yaml in the same directory is the client's own layer, and overwriting it destroys local state.

New organizations start with the toggle on, so turn it off before your MDM takes over the file. Leaving both on gives the file two writers.

Sign-in and verification

In SSO mode each user still signs in once: they open the local UI at http://localhost:8228 and complete the sign-in with your identity provider; recording starts from there, and grading runs on your backend, so the machines need no LLM provider access of their own. To verify a machine, run watcher doctor as the user, or let the audit script's checks stand in. To remove the client, run watcher uninstall as the user (it keeps ~/.apollo_monitor/; remove that too if the machine is being wiped of Watcher data).

Dev containers on managed machines

An MDM installs the client on the machine. It does not reach inside the containers that machine runs, so a developer whose coding agent runs in a dev container is unmonitored even on a fully rolled-out fleet.

The fix is a template each repository adopts, covered on Dev containers, which starts a client in the container and reuses the machine's sign-in. Publish it with your deployment URL and pinned client version already filled in. If repository processes are not trusted with the machine's Watcher sign-in, use the hardening guidance to design a separate boundary.