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, notarizedaiwatch 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:
| File | Purpose |
|---|---|
aiwatch-<version>-macos-arm64.pkg | Signed + notarized installer (single aiwatch binary + scan & enroll LaunchAgents + hook-install LaunchDaemon) |
com.runlayer.aiwatch.pppc.mobileconfig | Full Disk Access / TCC grants (upload as-is) |
com.runlayer.aiwatch.loginitems.mobileconfig | Pre-approves the bundled LaunchAgents on macOS 13+ (upload as-is; LabelPrefix=com.runlayer.aiwatch covers all current and future user-context units) |
.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 hooksubcommand of the singleaiwatchbinary at/usr/local/lib/runlayer/aiwatch/. AI coding clients (Cursor, Claude Code, Codex, Hermes) invoke it via the command stringaiwatch hook --client <name>that the bootstrap daemon writes into their config — no separate binary or symlink. -
Two extra
launchdunits split across privilege contexts so the strict-ordered enroll → hook-install sequence runs in the only context each half can succeed from:Both Enforce units short-circuit at aUnit Type Runs as Command Schedule Purpose com.runlayer.aiwatch.plistLaunchAgent ( /Library/LaunchAgents/)logged-in user aiwatch scanRunAtLoad+StartInterval=900Detect — scheduled scans (covered by Detect docs; listed here for completeness) com.runlayer.aiwatch.enroll.plistLaunchAgent ( /Library/LaunchAgents/)logged-in user aiwatch enrollRunAtLoad+StartInterval=3600Enforce step 1 — exchange MDM-pushed EnrollmentKeyfor 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/)root aiwatch 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 .pkginstall, then falls back to hourly./bin/shgate whenEnrollmentKeyis absent from the Configuration Profile, so scan-only fleets pay zero hourly noise.
.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 theHost / OrgApiKey fields used by Detect, enforce reads five more keys from the same com.runlayer.aiwatch preference domain:
| Key | Required | Purpose |
|---|---|---|
EnrollmentKey | Yes (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). |
Username | Optional | Override for the enrollment body. Defaults to getpass.getuser(). Set to a stable email when usernames collide across the org. |
DeviceName | Optional | Override for the enrollment body. Defaults to socket.gethostname(). |
Enforcement | Optional (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). |
Sessions | Optional (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. |
Creating an Enrollment Key
Creating an Enrollment Key
Enrollment keys let MDM-deployed devices exchange themselves for a per-user API key without each user logging in interactively.
Navigate to Enrollment Keys
Go to Settings in the Runlayer dashboard and select the Enrollment Keys tab.
Configure the Key
- Name (required): a descriptive label (e.g., “Production MDM — Enforce”).
- Description (optional): context about the key’s purpose.
How enforce bootstrap fires on macOS
Twolaunchd-managed units, one per privilege context. Step 2 hard-fails until step 1 has succeeded for the console user.
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), theaiwatch 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:
~/.runlayer/.enrollment-attempt) prevents a misconfigured key from hammering /enroll on every hook fire.
Edit the tenant-config profile
Open
For a monitoring-only rollout, also edit the literal
com.runlayer.aiwatch.config.mobileconfig in a text editor and replace these placeholders:| Placeholder | Replace with |
|---|---|
REPLACE_WITH_TENANT_HOST | e.g. https://your-instance.runlayer.com |
REPLACE_WITH_ORG_API_KEY | the rl_org_... secret (required for Detect) |
REPLACE_WITH_ENROLLMENT_KEY | the rl_enroll_... value from the dashboard (required for Enforce) |
REPLACE_WITH_USERNAME_OR_LEAVE_BLANK | leave blank to default to getpass.getuser() |
REPLACE_WITH_DEVICE_NAME_OR_LEAVE_BLANK | leave blank to default to socket.gethostname() |
<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).Upload the three Configuration Profiles
For each
.mobileconfig (tenant config edited above, plus PPPC and Login Items as-is):- Management → macOS → Profiles → Custom Profile.
- Upload the
.mobileconfig. - Assign to your target device groups.
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).Upload the .pkg as a Custom Package
- Management → macOS → Applications → Add a New App → Custom Apps.
- Upload
aiwatch-<version>-macos-arm64.pkg. - Assign to the same device groups.
.pkg install (so hooks land within ~60 s of enroll completing), then falls back to hourly.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.