Skip to main content
Migrating from the script-based Detect deployment? Run Clean Up Script-Based macOS Detect Deployment before rolling out the .pkg so the old runlayer-scan artifacts don’t conflict with com.runlayer.aiwatch.

Overview

A signed, notarized aiwatch binary installs once per device via .pkg. Tenant config (host + org API key, plus an enrollment key for Enforce) is pushed via MDM Configuration Profile. The .pkg bundles up to three launchd units: a scan LaunchAgent (user, default 15 min) for Detect, plus — when an EnrollmentKey is set — an enroll LaunchAgent (user, every 60 min, runs aiwatch enroll) and a hook-install LaunchDaemon (root, fast-retries every 60 s for the first 10 min after install then hourly, runs aiwatch setup hooks install --mdm) for Enforce.

Prerequisites

  • Devices enrolled via UAMDM (User-Approved MDM) or DEP/ADE. TCC payloads are ignored on manually-enrolled MDM.
  • An organization API key with the Detect Scan role minted in Settings → Organization API keys in the Runlayer dashboard. Record the secret value (rl_org_...).
  • Your Runlayer tenant host URL (e.g. https://your-instance.runlayer.com).
Apple Silicon only for now. The current release ships an arm64 .pkg.

Artifacts

The package is a .zip named aiwatch-<version>-macos-arm64.zip. Contents:
FilePurpose
aiwatch-<version>-macos-arm64.pkgSigned + notarized installer (single aiwatch binary + scan & enroll LaunchAgents + hook-install LaunchDaemon)
com.runlayer.aiwatch.pppc.mobileconfigFull Disk Access / TCC grants (upload as-is)
com.runlayer.aiwatch.loginitems.mobileconfigPre-approves the bundled LaunchAgents on macOS 13+ (upload as-is; LabelPrefix=com.runlayer.aiwatch covers all current and future user-context units)
Contact your Runlayer account team if you don’t have the .zip yet.
Deploy the three Configuration Profiles before the .pkg. Profiles must land in /Library/Managed Preferences/ and TCC before the bundled LaunchAgent’s first scan tick — otherwise aiwatch logs host not configured and TCC denies project-config reads until the next MDM sync.

Deployment

Enforce builds on the same .pkg and Configuration Profiles as Detect. Complete the Detect deployment for your MDM first; then add the enrollment-key fields below and verify the bundled enroll LaunchAgent + bootstrap LaunchDaemon. No additional MDM-side scripts are required on macOS.

What enforce adds to the .pkg

The signed .pkg already ships everything enforce needs:
  • The Enforce hook fires via the aiwatch hook subcommand of the single aiwatch binary at /usr/local/lib/runlayer/aiwatch/. AI coding clients (Cursor, Claude Code, Codex, Hermes) invoke it via the command string aiwatch hook --client <name> that the bootstrap daemon writes into their config — no separate binary or symlink.
  • Two extra launchd units split across privilege contexts so the strict-ordered enroll → hook-install sequence runs in the only context each half can succeed from:
    UnitTypeRuns asCommandSchedulePurpose
    com.runlayer.aiwatch.plistLaunchAgent (/Library/LaunchAgents/)logged-in useraiwatch scanRunAtLoad + StartInterval=900Detect — scheduled scans (covered by Detect docs; listed here for completeness)
    com.runlayer.aiwatch.enroll.plistLaunchAgent (/Library/LaunchAgents/)logged-in useraiwatch enrollRunAtLoad + StartInterval=3600Enforce step 1 — exchange MDM-pushed EnrollmentKey for a per-user API key in the user’s keychain. Must run as the user; root has no user keychain.
    com.runlayer.aiwatch.bootstrap.plistLaunchDaemon (/Library/LaunchDaemons/)rootaiwatch setup hooks install --mdmRunAtLoad + KeepAlive(SuccessfulExit=false) + ThrottleInterval=60 + StartInterval=3600Enforce step 2 — write Runlayer hook entries into Cursor and Codex enterprise configs plus the console user’s Claude Code and Hermes configs. Refuses to write until the enroll agent has populated the user’s enrollment marker (strict-order gate). Fast-retries every 60 s for the first 10 min after .pkg install, then falls back to hourly.
    Both Enforce units short-circuit at a /bin/sh gate when EnrollmentKey is absent from the Configuration Profile, so scan-only fleets pay zero hourly noise.
Because the enroll agent + hook-install daemon both ship inside the .pkg and launchd runs them in the correct context, there is no MDM-side enforce script to deploy on macOS.

Additional Managed Preferences fields for enforce

In addition to the Host / OrgApiKey fields used by Detect, enforce reads five more keys from the same com.runlayer.aiwatch preference domain:
KeyRequiredPurpose
EnrollmentKeyYes (enforce)One-time rl_enroll_... key the enroll LaunchAgent exchanges at /api/v1/mdm/enroll for a per-user API key. Mint in the Runlayer dashboard (see below).
UsernameOptionalOverride for the enrollment body. Defaults to getpass.getuser(). Set to a stable email when usernames collide across the org.
DeviceNameOptionalOverride for the enrollment body. Defaults to socket.gethostname().
EnforcementOptional (bool)<true/> (or omitted) blocks policy-violating tool calls. Set <false/> to roll out in monitoring-only mode — hooks still forward events but never block. Equivalent to --no-enforcement on the manual runlayer setup hooks --install path. Picked up by aiwatch hook on next fire (no restart, no .pkg reinstall).
SessionsOptional (bool)<true/> (or omitted) installs the full hook set — all event/session hooks for Sessions telemetry, plus the enforcement hooks. Set <false/> to install enforcement hooks only. The default full-hook behavior matches passing --event-hooks / --all-events on the manual path. Applied at install time by the bootstrap LaunchDaemon; a flipped value is picked up on the next hourly tick (no .pkg reinstall). Independent of Enforcement — blocking is governed by Enforcement, hook coverage by Sessions.
Enrollment keys let MDM-deployed devices exchange themselves for a per-user API key without each user logging in interactively.
1

Navigate to Enrollment Keys

Go to Settings in the Runlayer dashboard and select the Enrollment Keys tab.
2

Create a New Key

Click + Create Enrollment Key.
3

Configure the Key

  • Name (required): a descriptive label (e.g., “Production MDM — Enforce”).
  • Description (optional): context about the key’s purpose.
4

Copy the Key

Copy the generated key (starts with rl_enroll_) and paste it into the EnrollmentKey field in your com.runlayer.aiwatch Configuration Profile.
Enrollment keys are shown only once. Store them securely and treat them like passwords. Rotating the key later is a profile re-push — no .pkg reinstall.
The same key value can target multiple devices — it identifies the deployment, not the device.

How enforce bootstrap fires on macOS

Two launchd-managed units, one per privilege context. Step 2 hard-fails until step 1 has succeeded for the console user.
                            ┌───────────────────────────────────────────┐
              pkg install   │ /Library/LaunchAgents/                    │
                (root)  ──▶ │   com.runlayer.aiwatch.enroll.plist       │
                            │ /Library/LaunchDaemons/                   │
                            │   com.runlayer.aiwatch.bootstrap.plist    │
                            └──────────────────┬────────────────────────┘
                                               │ launchd loads each
                                               │ into its own domain
                       ┌───────────────────────┴───────────────────────┐
                       ▼                                               ▼
       ┌─────────────────────────────────┐         ┌─────────────────────────────────┐
       │ gui/<console-uid>/              │         │ system/                         │
       │  com.runlayer.aiwatch.enroll    │         │  com.runlayer.aiwatch.bootstrap │
       │  (runs as logged-in user)       │         │  (runs as root)                 │
       │  RunAtLoad + every 3600s        │         │  RunAtLoad + KeepAlive (60s     │
       │                                 │         │  fast-retry, 10-min window) +   │
       │                                 │         │  every 3600s thereafter         │
       │                                 │         │                                 │
       │  /usr/local/bin/aiwatch enroll  │         │  /usr/local/bin/aiwatch         │
       │   EnrollmentKey → per-user      │         │      setup hooks install --mdm  │
       │   API key in user's keychain    │         │   writes hook configs for       │
       │   + ~/.runlayer/config.yaml     │         │   Cursor/Codex enterprise +     │
       │   host marker                   │         │   console-user Claude/Hermes    │
       │                                 │         │   config files                  │
       └──────────────┬──────────────────┘         └──────────────┬──────────────────┘
                      │ writes host marker                        │ checks for host marker
                      │ to ~/.runlayer/config.yaml                │ before writing; exit 4
                      └────────────────────┬─────────────────────▶│ until enroll has run
                                           │ strict-order gate

                              hook configs installed
                         (Cursor / Claude Code / Codex / Hermes)
Both units exit at the end of each tick. Successive ticks short-circuit: enroll skips if a user credential already exists, and hook install is idempotent (re-writes the same command strings, preserves third-party entries). The bootstrap daemon also short-circuits with exit 4 if no console user has enrolled yet — within the first 10 min after .pkg install the KeepAlive re-fires every 60 s so hooks land within ~60 s of enroll completing; after that window it exits 0 on credential-gate failure so launchd idles until the next hourly tick.

Lazy enrollment fallback

If something wipes the user keychain between enroll-agent ticks (or the user clears it themselves), the aiwatch hook path itself runs a one-shot enrollment when it fires for an AI client. It uses the same EnrollmentKey from the Configuration Profile, persists the new user API key, and proceeds — so a single hook fire transparently re-provisions the device. This path is self-healing only — the steady-state flow is the enroll agent + bootstrap daemon. If the dashboard reports aiwatch.lazy_enrollment_fallback_hit events on a device, something interfered with one of those two units (typically the enroll agent was bootout’d or the user keychain reset); check both on the device:
launchctl print "gui/$(id -u)/com.runlayer.aiwatch.enroll"
sudo launchctl print system/com.runlayer.aiwatch.bootstrap
A 60-second cooldown file (~/.runlayer/.enrollment-attempt) prevents a misconfigured key from hammering /enroll on every hook fire.
1

Edit the tenant-config profile

Open com.runlayer.aiwatch.config.mobileconfig in a text editor and replace these placeholders:
PlaceholderReplace with
REPLACE_WITH_TENANT_HOSTe.g. https://your-instance.runlayer.com
REPLACE_WITH_ORG_API_KEYthe rl_org_... secret (required for Detect)
REPLACE_WITH_ENROLLMENT_KEYthe rl_enroll_... value from the dashboard (required for Enforce)
REPLACE_WITH_USERNAME_OR_LEAVE_BLANKleave blank to default to getpass.getuser()
REPLACE_WITH_DEVICE_NAME_OR_LEAVE_BLANKleave blank to default to socket.gethostname()
For a monitoring-only rollout, also edit the literal <true/> under the Enforcement key to <false/> in the same file. Default (<true/>) blocks policy-violating tool calls. To install enforcement hooks only — no Sessions telemetry — edit the <true/> under the Sessions key to <false/> (default installs the full event/session hook set; see Sessions telemetry).
2

Upload the three Configuration Profiles

For each .mobileconfig (tenant config edited above, plus PPPC and Login Items as-is):
  1. Management → macOS → Profiles → Custom Profile.
  2. Upload the .mobileconfig.
  3. Assign to your target device groups.
PPPC + Login Items profiles are pre-pinned to Developer ID team AF2M8HC7A2 — no edits required. The Login Items profile’s LabelPrefix=com.runlayer.aiwatch rule pre-approves the scan and enroll LaunchAgents (the bootstrap LaunchDaemon runs as root and is exempt from Login Items).
3

Upload the .pkg as a Custom Package

  1. Management → macOS → Applications → Add a New App → Custom Apps.
  2. Upload aiwatch-<version>-macos-arm64.pkg.
  3. Assign to the same device groups.
No additional Custom Command payload is required — the bundled enroll LaunchAgent + bootstrap LaunchDaemon handle enroll + hook installation. Both fire at login; the bootstrap daemon fast-retries every 60 s for the first 10 min after .pkg install (so hooks land within ~60 s of enroll completing), then falls back to hourly.
Rotating the Enrollment Key later: edit com.runlayer.aiwatch.config.mobileconfig, bump PayloadVersion on both the inner and outer payloads, re-upload. The next bootstrap tick on each device picks up the new key. No .pkg reinstall.