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

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.sh sample. 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. This includes downgrades during a rollback.

    When watcher doctor hooks reports drift, run hooks install --yes to repair it. This command auto-approves the Codex config.toml changes. 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:

  1. Downloads the release
  2. Verifies its checksum
  3. Installs under the user's ~/.local
  4. Records the backend target beside the binary (the client reads it on every start and refuses to start without one)
  5. 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.

  1. 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 --yes
    jq '{
    allowManagedHooksOnly: true,
    env: {CLAUDE_CODE_FORCE_SESSION_PERSISTENCE: "1"},
    hooks: .hooks,
    statusLine: .statusLine
    }' "$HOME/.claude/settings.json" > watcher-managed-settings.json

    Review 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.

    allowManagedHooksOnly disables 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.

  2. Have your MDM deliver the reviewed payload and run this installation step as root. Here /path/to/watcher-managed-settings.json is 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 install updates the user config, not this file.

  3. 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 /status to confirm the file-based managed settings are active. Use /hooks to 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 hooks checks 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:

  • install only 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.
  • enable turns the start-on-login toggle on in the user's settings.
  • start is 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:

  1. Remove the WATCHER_HEADER_* variables from the environment
  2. Run watcher hooks clear-headers as 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.