Deploy via MDM#
Push FirstOps to every developer machine through your MDM platform. The daemon enrolls automatically, installs hooks, and begins enforcement — no developer action required.
How zero-touch enrollment works#
Traditional security tool rollouts follow a two-step pattern: IT pushes the binary, then chases developers to activate it. FirstOps eliminates the second step. Your MDM runs a deployment script that installs the CLI, writes a system config with an enrollment token, and starts the daemon. On first boot, the daemon enrolls itself — generating a cryptographic identity, creating a principal, and starting governance.
Developers can optionally run fo auth login later to verify their identity via SSO, which upgrades their status from ENROLLED to ACTIVE. But governance is already running from the moment the script executes.
Prerequisites#
- A FirstOps account with admin access
- An MDM platform that supports running shell scripts on macOS devices (Jamf, JumpCloud, Intune, or similar)
Step 1: Generate an enrollment token#
Open the FirstOps dashboard and navigate to Enrollment Tokens in the sidebar (under Infrastructure).
Click Generate Token. Configure two settings:
- Max Enrollments — How many devices can use this token. Set to
0for unlimited. For a 50-person team, a value of60gives headroom for multi-device users. - Expires In — When the token stops accepting new enrollments. Options: Never, 24 hours, 7 days, 30 days, or 90 days. For initial rollouts, "Never" is fine — you can revoke the token manually when done.
Click Generate. The dashboard creates the token and immediately shows the Deploy via MDM modal.
The token value is shown once. The deployment script in the modal contains the full token. If you close this modal without copying the script, you will need to generate a new token.
Step 2: Copy the deployment script#
The Deploy via MDM modal displays a ready-to-use bash script with your enrollment token embedded. Click Copy Script to copy it to your clipboard.
The script does four things:
- Installs the
foCLI by downloading the latest release binary to/usr/local/bin/fo - Writes system config to
/etc/firstops/config.yamlwith your enrollment token and API URL - Installs a LaunchAgent at
/Library/LaunchAgents/com.firstops.fo-daemon.plistso the daemon starts automatically on user login - Starts the daemon immediately for the currently logged-in user via
launchctl
The script runs as root (required by MDM), but the daemon runs as the logged-in user. Credentials are stored in the user's home directory at ~/.config/firstops/, not in a system-wide location.
Here is the full script for reference:
#!/bin/bash
set -euo pipefail
# FirstOps zero-touch enrollment
# Push via: JumpCloud Commands | Jamf Scripts | Intune Shell Scripts
ENROLLMENT_TOKEN="et_<your-enrollment-token>"
API_URL="https://api.firstops.dev"
# 1. Install CLI (latest release)
curl -fsSL https://firstops-cli-releases.s3.amazonaws.com/install.sh | sh
# 2. Write enrollment config
mkdir -p /etc/firstops
cat > /etc/firstops/config.yaml <<EOF
api_url: "$API_URL"
enrollment_token: "$ENROLLMENT_TOKEN"
EOF
chmod 644 /etc/firstops/config.yaml
# 3. Install LaunchAgent (runs as each logged-in user, not root)
cat > /Library/LaunchAgents/com.firstops.fo-daemon.plist <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.firstops.fo-daemon</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/fo</string>
<string>daemon</string>
<string>run</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>ThrottleInterval</key>
<integer>60</integer>
<key>StandardOutPath</key>
<string>/tmp/firstops-daemon.log</string>
<key>StandardErrorPath</key>
<string>/tmp/firstops-daemon.log</string>
</dict>
</plist>
PLIST
# 4. Start daemon for current console user (immediate, no logout required)
CONSOLE_USER=$(stat -f "%Su" /dev/console 2>/dev/null || echo "")
if [ -n "$CONSOLE_USER" ] && [ "$CONSOLE_USER" != "root" ]; then
CONSOLE_UID=$(id -u "$CONSOLE_USER")
launchctl bootout gui/$CONSOLE_UID/com.firstops.fo-daemon 2>/dev/null || true
launchctl bootstrap gui/$CONSOLE_UID /Library/LaunchAgents/com.firstops.fo-daemon.plist
fi
echo "FirstOps enrolled successfully."
You do not need to edit the script. The version generated by the dashboard has your enrollment token and API URL pre-filled. Copy it as-is.
Step 3: Push via your MDM#
Paste the copied script into your MDM platform's remote script feature and assign it to the target device group. The instructions below cover the three most common platforms.
Jamf Pro#
- Go to Settings > Computer Management > Scripts
- Click New, give it a name (e.g., "FirstOps Enrollment"), and paste the script into the Script tab
- Go to Computers > Policies, create a new policy
- Under Scripts, add the script you created
- Set Trigger to "Recurring Check-in" and Execution Frequency to "Once per computer"
- Under Scope, select the Smart Group containing your target machines
- Click Save
The script runs the next time each scoped device checks in with Jamf (typically within 15 minutes).
Jamf Pro documentation: Running Scripts
JumpCloud#
- Go to Device Management > Commands
- Click + to create a new command
- Set Command Type to "Mac"
- Paste the script into the Command field
- Set Run As to "root"
- Under Devices, select the device group you want to target
- Click Save and then Run Now (or schedule for later)
JumpCloud runs the command on the next device check-in. You can monitor status from the Commands page.
JumpCloud documentation: Create a Command
Microsoft Intune#
- Go to Devices > macOS > Shell scripts
- Click Add, upload the script as a
.shfile - Set Run script as signed-in user to "No" (runs as root)
- Set Max number of retries if script fails to
3 - Assign to a device group under the Assignments tab
- Click Save
Intune pushes the script on the next device sync. Check deployment status under Monitor > Device status.
Microsoft Intune documentation: macOS shell scripts
Other MDMs#
Any MDM that runs shell scripts on macOS devices works with FirstOps. Paste the script, configure it to run as root, scope it to your target device group, and deploy. The script is self-contained — no external dependencies beyond curl.
Step 4: Verify enrollment#
After the script runs on target devices, verify enrollment from the dashboard:
Enrollment Tokens page — The Used column increments for each device that enrolls with that token. If your token shows "Max: 50, Used: 47", 47 devices have enrolled.
Principals page — New principals appear for each enrolled device. MDM-enrolled principals are created with the pattern {macOS-username}@{your-tenant-domain}.
Daemon Statuses page — Shows live health of every daemon across your organization. Enrolled devices appear with their hostname, OS version, last heartbeat time, and which AI agents are hooked.
You can also verify from the device itself:
fo status
This outputs the daemon's connection state, principal ID, and active hooks.
What happens on the device#
When the deployment script runs, the daemon starts and executes the following sequence automatically:
- The LaunchAgent starts
fo daemon runas the logged-in user - The daemon checks for credentials at
~/.config/firstops/credentials.json— none exist yet - The daemon reads the enrollment token from
/etc/firstops/config.yaml - The daemon collects device context: local macOS username, hostname, serial number, and OS version
- The daemon generates an ES256 (ECDSA P-256) keypair for DPoP authentication
- The daemon calls
POST /api/v1/enrollwith the token, device context, and DPoP public key - The backend validates the token, constructs the principal email as
{username}@{tenant-domain}, and creates the user and principal - The daemon receives an access token, refresh token, and device ID
- The daemon saves credentials to
~/.config/firstops/credentials.jsonand the DPoP private key to~/.config/firstops/dpop_key.pem - The daemon redacts the enrollment token from
/etc/firstops/config.yaml— the raw token is no longer on disk - The daemon installs hooks into detected AI agents (Claude Code, Cursor, etc.), starts the MCP proxy, and begins heartbeating
If the network is unavailable during first boot, the daemon exits and the LaunchAgent restarts it after 60 seconds. Enrollment retries automatically on each restart until it succeeds.
Identity: how the principal email is constructed#
The daemon sends the local macOS username to the enrollment API. The backend constructs the principal's email address by combining it with your tenant's domain:
{local macOS username} + "@" + {tenant domain}
This works because most organizations using SSO-based device enrollment (via Okta, Azure AD, or JumpCloud) create local macOS accounts that match the email prefix. If your organization uses a different username convention, contact support — a username-to-email mapping is available for non-standard setups.
Optional: upgrade to verified identity#
MDM-enrolled principals have an ENROLLED status. They are fully governed — policies enforce, hooks intercept, audit logs capture — but they do not have dashboard access.
A developer can upgrade to ACTIVE status at any time by running:
fo auth login
This opens a browser-based SSO flow. On success, the existing ENROLLED principal is upgraded to ACTIVE with a verified identity. The same principal, connections, and audit history are preserved — no data is lost or duplicated.
Managing enrolled devices#
Revoking a token#
Navigate to Enrollment Tokens and click Revoke next to the token. Revocation is immediate: no new devices can enroll with that token. Devices already enrolled are unaffected — their credentials are independent of the token.
Rotating tokens#
Generate a new token, update the deployment script in your MDM with the new token, and revoke the old one. Devices that already enrolled keep working. Only devices that have not yet enrolled need the updated script.
Multi-device users#
If a developer enrolls from two machines (e.g., a MacBook Pro and an iMac), both devices share the same principal because the macOS username is the same. Each device gets its own DPoP keypair and its own daemon credentials. Revoking one device does not affect the other.
Re-enrollment#
If a device's credentials are deleted (e.g., the user clears ~/.config/firstops/), the daemon re-enrolls on next boot using the enrollment token in /etc/firstops/config.yaml. If the token was already redacted after first enrollment, the admin must re-push the deployment script.
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
| Daemon not running after script push | LaunchAgent not loaded for current user | Run launchctl list | grep firstops to check. If missing, run the bootstrap command from step 4 of the script manually. |
| Enrollment fails: "token exhausted" | Token reached its max enrollment count | Generate a new token with a higher limit or set to unlimited (0). |
| Enrollment fails: "token expired" | Token past its expiration date | Generate a new token. |
| Enrollment fails: "token revoked" | Admin revoked the token | Generate a new token and update the MDM script. |
| Wrong email on principal | Local macOS username does not match email prefix | Configure a username-to-email mapping in admin settings. |
| Daemon stuck, not retrying enrollment | A permanent failure wrote a sentinel file | Delete ~/.config/firstops/enrollment_failed on the device and restart the daemon. A new token clears this automatically. |
| Script fails on Linux or Windows | The deployment script is macOS-only | Linux and Windows support is on the roadmap. |
fo status shows no hooks | No supported AI agents detected on the device | Install Claude Code, Cursor, or another supported agent, then restart the daemon. |
| Daemon logs show network errors | Device cannot reach the FirstOps API | Verify the API URL is reachable from the device. Check proxy and firewall rules. |
Daemon logs are written to /tmp/firstops-daemon.log. Check this file first when diagnosing enrollment issues.
Security#
Token storage. Enrollment tokens are bcrypt-hashed in the database. The raw token value exists only in the deployment script and on the device's /etc/firstops/config.yaml until enrollment succeeds, at which point the daemon redacts it.
Device binding. Each device generates a unique ES256 DPoP keypair at enrollment. Credentials are cryptographically bound to the device's private key — copying credentials.json to another machine without the corresponding dpop_key.pem renders them useless.
Least privilege. MDM-enrolled principals are assigned to the Default access group, which has the most restrictive policy set. Admins can reassign access groups from the dashboard after enrollment.
Audit trail. Every enrollment is logged with device metadata (hostname, serial number, OS version) for forensic visibility. Re-enrollments are logged as separate audit events.
For a deeper explanation of DPoP authentication and the identity model, see Identity and Auth.