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.
Upload PPPC profile
Computers → Configuration Profiles → Upload. Upload
com.runlayer.aiwatch.pppc.mobileconfig as-is. Scope to your AI Watch Smart Group.The profile pins Full Disk Access to Developer ID team AF2M8HC7A2 + identifier com.runlayer.aiwatch — only the Anysource-signed binary satisfies the CodeRequirement. No edits.Upload Login Items profile
Computers → Configuration Profiles → Upload. Upload
com.runlayer.aiwatch.loginitems.mobileconfig as-is. Scope to the same Smart Group.Pre-approves the bundled scan + enroll LaunchAgents on macOS 13+ via the LabelPrefix=com.runlayer.aiwatch rule, so users don’t see “Background Item Added” notifications. (The bootstrap LaunchDaemon runs in the system domain and doesn’t need login-items approval.)Create the tenant-config profile via JSON Schema
- Computers → Configuration Profiles → New → Application & Custom Settings → External Applications → Add → Custom Schema.
- Preference Domain:
com.runlayer.aiwatch. - Paste the contents of
com.runlayer.aiwatch.jamf.schema.jsoninto the schema source field. - Fill in:
- Tenant Host (e.g.
https://your-instance.runlayer.com) - Org API Key (the
rl_org_...secret — required for Detect; safe to include alongside Enforce) - Enrollment Key (the
rl_enroll_...value from the Enrollment Keys tab — required for Enforce) - Username / Device Name — leave blank to default to
getpass.getuser()/socket.gethostname(). - Enforcement — leave checked (default) to block policy-violating tool calls; uncheck for a monitoring-only rollout (hooks still forward events but never block).
- Sessions — leave checked (default) to install the full event/session hook set for Sessions telemetry; uncheck to install enforcement hooks only.
- Tenant Host (e.g.
- Scope to the same Smart Group.
Upload the .pkg and create a deployment Policy
Jamf → Computers → Management Settings → Computer Management → Packages → New. Upload
aiwatch-<version>-macos-arm64.pkg.Create a Policy to deploy the package: Computers → Policies → New. Add the package, set Trigger to Recurring Check-in and Frequency to Once per computer. Scope to the same Smart Group.No additional script payload is required — the bundled enroll LaunchAgent (user) handles enrollment and the bundled bootstrap LaunchDaemon (root) installs hook configs. 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..pkg reinstall.