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.shsample and run it as the logged-in user.Set
WATCHER_API_URLin the remediation script. The running client's built-in scheduler follows the server version, including downgrades during a rollback.When
watcher doctor hooksreports drift,hooks install --yesrepairs it and auto-approves the Codexconfig.tomlchanges 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.