---
description: Roll out the Watcher client with an MDM, protect agent hook configuration, and manage startup and settings.
---

> Part of the Watcher docs. Index of all pages, with summaries: https://watcher-docs.apolloresearch.ai/llms.txt
> Before upgrading Watcher, read the release pages and full changelogs for every version crossed: https://watcher-docs.apolloresearch.ai/releases/index.md
> Found an issue with this page? Report it: https://github.com/ApolloResearch/watcher-bin/issues/new?title=Docs%20feedback%3A%20MDM%20rollout&body=Page%3A%20MDM%20rollout%0AURL%3A%20https%3A%2F%2Fwatcher-docs.apolloresearch.ai%2Fself-hosting%2Fmdm%2F%0A%0A

# MDM rollout

An MDM can install and maintain the [Watcher client](../client/index.md) 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](client-deployment.md#bake-the-client-into-a-vm-image)
instead.

For downloadable audit, install, and uninstall scripts, see the
[Iru provider sample](provider-samples/iru.md).

**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](#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`](../assets/mdm/iru/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](../supported-agents.md)). 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](https://code.claude.com/docs/en/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:

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

   ```sh
   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](../concepts/sessions-and-trajectories.md)
   appears in the [Analyzer](../concepts/analyzer.md). `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](#proxy-mode-headers) for
details.

```sh
#!/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"
```

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:

```sh
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](client-deployment.md#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:

```sh
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](../concepts/organization-settings.md#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.

## Sign-in and verification

In SSO mode each user still signs in once. They open the local UI at
`http://localhost:8228`. 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](../client/devcontainer.md). 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](../client/devcontainer-hardening.md) to design a separate
boundary.

## Provision renewable proxy credentials

For deployments using [external credential helpers](../client/credential-helpers.md),
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.
