MDM rollout
An MDM can install and maintain the Watcher client across a fleet of managed machines. This avoids having every developer run the installer. 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 default hook configuration is user-writable. Watcher registers Claude
Code hooks in ~/.claude/settings.json. It registers Codex hooks in
~/.codex/hooks.json (or $CODEX_HOME/hooks.json when CODEX_HOME is set).
The developer can edit or remove these hooks. Processes running as that user can
also edit or remove them. Running the installer through an MDM does not change
this. See Protect agent hook configuration
for the shared requirements and a Claude Code example on macOS.
The audit-and-remediate pattern
Run two scripts per machine on a schedule. The MDM runs them every 15 minutes to daily.
-
An audit script decides whether the machine needs work. It checks if the binary or its runtime tree is missing. It checks if the logged-in user's hooks or login agent are absent. It checks if the daemon is not answering.
-
A remediation script installs Watcher when absent. It repairs hooks and autostart. It restarts the daemon. Use the downloadable
remediate-macos.shsample. 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. This includes downgrades during a rollback.When
watcher doctor hooksreports drift, runhooks install --yesto repair it. This command auto-approves the Codexconfig.tomlchanges. An interactive install would prompt for consent (see supported agents). An MDM rollout 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. It 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)
- Registers the agent hooks
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. The running client's built-in scheduler owns version changes.
Protect agent hook configuration
For each coding agent in your fleet, deploy Watcher's hook definitions through that agent's managed configuration. Use its managed-hooks-only policy to restrict user and project hooks. Protect the policy file from writes by ordinary users. Protect its parent directories from writes by ordinary users. Verify that the deployed agent version loads Watcher's managed hooks before rolling the policy out. The policy keys and configuration paths differ between agents.
Protecting configuration does not protect the executable. The default
installer places Watcher under the user's ~/.local. A managed hook that still
calls that installation runs user-replaceable code. Protecting hook execution
requires more:
- An administrator-owned executable and runtime
- Parent directories that users cannot modify
- An administrator-controlled update process
The example below does not provide that process. It does not prevent a user from stopping Watcher or launching an unmonitored agent.
Example: Claude Code on macOS
Use an MDM file deployment. Put Claude Code's hook configuration in
/Library/Application Support/ClaudeCode/managed-settings.json. Make it owned
by root. This example protects the configuration from ordinary users. It does
not configure Codex or protect against local administrators.
Prepare the payload on a trusted test Mac. You need jq installed and a clean
account containing only Watcher's Claude Code hooks and status line. Install and
sign in to Watcher as described above. If your organization already distributes
Claude Code managed settings, merge these keys into that policy instead of
replacing it. A higher-priority server policy or MDM configuration profile can
prevent the file policy from taking effect. See
Claude Code's managed settings
for details.
-
As the test user, generate the hooks for the client release and deployment you intend to roll out, then export the policy:
watcher hooks install --yesjq '{allowManagedHooksOnly: true,env: {CLAUDE_CODE_FORCE_SESSION_PERSISTENCE: "1"},hooks: .hooks,statusLine: .statusLine}' "$HOME/.claude/settings.json" > watcher-managed-settings.jsonReview the payload before adding it to your MDM. Keep the generated hook events, timeouts, and environment values. Commands contain absolute executable paths. Prepare paths for each destination account or your fleet's shared installation. Do not promote files from developers' machines into managed policy during scheduled remediation.
allowManagedHooksOnlydisables user and project hooks. This includes the user-level Watcher entries. It also restricts status lines and plugin hooks. Test any other integrations your fleet needs. The generated Watcher hooks and status line are included in the managed payload. -
Have your MDM deliver the reviewed payload and run this installation step as root. Here
/path/to/watcher-managed-settings.jsonis the MDM-delivered file, stored in a directory ordinary users cannot modify:policy_dir="/Library/Application Support/ClaudeCode"/usr/bin/install -d -o root -g wheel -m 0755 "$policy_dir"/usr/bin/install -o root -g wheel -m 0644 \/path/to/watcher-managed-settings.json \"$policy_dir/managed-settings.json"Protect the directory as well as the file. A root-owned file inside a user-writable directory still lets the user replace it. Include the policy contents, ownership, and directory permissions in your MDM audit. Repeat the review and deployment when Watcher's generated hooks change.
watcher hooks installupdates the user config, not this file. -
On a test device, confirm that the developer cannot write to either the managed file or its directory. Start a new Claude Code session and use
/statusto confirm the file-based managed settings are active. Use/hooksto check that Watcher's hooks come from managed settings, then run a harmless tool call and confirm its session appears in the Analyzer.watcher doctor hookschecks the user configuration; it does not verify this managed policy.
Start on login
watcher autostart install writes the client's login agent. This is a macOS
LaunchAgent or Linux systemd user unit. At login the agent runs a launcher. The
launcher exits quietly when the start-on-login toggle is off. It starts the
client otherwise. By default the binary is its own launcher. This applies to
client 0.16.0 and later. Older clients always require --launcher.
A wrapper script is passed --autostart. This is how it learns the launch came
from the agent. It can check the toggle before it execs anything. The binary is
registered with no arguments. It recognizes the agent by itself. This
recognition is why a wrapper you supply can end in a bare exec of the binary
and still work against every client version. 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. It starts the client at login even when the toggle is off. Reinstalling the agent restores it. Upgrading again also 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. It
should export them. A custom WATCHER_BACKEND_PORT is the main case. The client
takes the deployment's sign-in mode from the backend. Nothing in the launcher
selects it. Proxy headers do not belong here either. Setup captures them into a
file that later launches read. See proxy-mode headers for
details.
#!/usr/bin/env bash
set -euo pipefail
# Environment the login agent needs: launchd provides none beyond PATH.
export WATCHER_BACKEND_PORT=8228
# 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"
After the packaged client binds its local listener, it records the port in
~/.apollo_monitor/backend-port. Plain watcher, watcher doctor, and
watcher hooks install reuse that port, including while the client is stopped.
An explicit WATCHER_BACKEND_PORT overrides the record. An alternate-port
foreground client preserves a running login daemon's recorded port. Before the client's
first startup, pass the custom port when installing hooks. Source checkouts do
not read or overwrite the packaged client's port record.
Register the agent as the logged-in user, not as root. MDM jobs run as root.
These commands write into the invoking user's home. They write the LaunchAgent
plist and the settings toggle. 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. No one else should
be able to execute it. Use 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:
installonly writes the plist into the user's~/Library/LaunchAgents. Launchd loads it at the next login. It needs no access to the user's GUI session.enableturns the start-on-login toggle on in the user's settings.startis the only step that touches the live session. It bootstraps the agent into the user's GUI launchd domain. 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. 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. This means when the installer runs and on
any later watcher hooks install. See
sending additional headers
for details. In the remediation script, set them inside the sudo -u invocation
that installs or repairs hooks. The identity value must come from your MDM's
trusted per-user directory record. This is normally the user's email address. It
is not supplied by Watcher. It must not come from user-controlled input. For
example:
dev_email="alice@example.com" # Populate from this user's MDM directory record.
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. Requests read that file. The login agent and
later launches need no header exports of their own. 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:
- Remove the
WATCHER_HEADER_*variables from the environment - Run
watcher hooks clear-headersas the user (the command refuses while the variables are still set)
Delivering managed settings
Your organization may distribute
managed client settings
by MDM rather than via Watcher. In this case, the "Distribute via Watcher"
toggle is off. Add a second audit-and-remediate pair. It 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. Overwriting it destroys local state.
New organizations start with the toggle on. Turn it off before your MDM takes over the file. Leaving both on gives the file two writers.
Ship exactly the version saved in the Analyzer, and save one there before you roll out. Your backend grades each call the machine sends it against the saved document, not the file on the machine. A hand-edited or out-of-date file means the machine and your backend judge calls by different settings. With no saved document, your backend uses the built-in defaults.
Sign-in and verification
In SSO mode each user still signs in once: from the Watcher menu bar on macOS,
at http://localhost:8228, or with watcher login. They complete the sign-in
with your identity provider.
Recording starts from there. Grading runs on your backend. The machines need no
LLM provider access of their own.
To verify a machine, run watcher doctor as the user. You can also 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. 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. It is covered on Dev containers. It 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.
Provision renewable proxy credentials
For deployments using external credential helpers,
install the trusted company CLI and run watcher auth helper set as each
developer account. Use an absolute installed executable path and pass literal
arguments after --. Do not provision as root or distribute a shared token
cache. Verify watcher auth helper check under the same account and background
environment that runs Watcher. Initial company sign-in happens outside Watcher.