Skip to main content

MDM reference

Depth behind Bulk enrollment with your MDM.

SCIM or MDM

The two solve different halves of fleet onboarding.

MechanismSource of truthCredentialBest for
Inbound SCIMYour IdP owns users, groups, teams, roles, activation, and offboarding.Static SCIM bearer sent to <gateway>/scim/v2. Only its hash is stored.Identity lifecycle and immediate deactivation of existing keys.
MDM provisioningYour device manager runs enrollment on each endpoint.Reusable enp_… bootstrap token. Each device receives its own ark_… key.Installing tool config and enrolling managed or headless machines.

A SCIM deactivation blocks that identity's already-issued key. Revoking an enp_… token only stops future device enrollments. Setup: Set up inbound SCIM.

Managed Macs

The profile is a managed-preferences payload for ai.anyray.connect with two keys: EnrollmentUrl (the reusable enrollment link, a credential) and EnrollmentEmail ($EMAIL). Who can enroll in the console shows the email domains baked into the profile. Changing domains needs a new profile.

Create & download appears only after the public signed installer is published. The profile is shown once. If it is lost, create a replacement and revoke the old entry under MDM Users.

Mint the profile from the admin API
umask 077
curl --fail-with-body -sS -X POST \
https://gateway.example.com/admin/provisioning-tokens \
-H "Authorization: Bearer $ANYRAY_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"kind": "mdm",
"identityMode": "email",
"proofMode": "managed-preference",
"allowedDomains": ["example.com"]
}' | jq -er '.mobileconfig' > anyray-enroll.mobileconfig
Retries, repair, and opting out

If the profile lands first, the package starts enrollment for the signed-in desktop user. If the package arrives first, its LaunchAgent waits for the managed preference. A Mac with no desktop user enrolls at the next login.

The desktop app starts enrollment on its first doctor poll. A temporary failure retries in 30 seconds and doubles to a 10-minute cap while the dot stays yellow. A permanent rejection turns the dot red with "contact your admin" and re-arms after 30 minutes. The package runner keeps its own cadence:

  • Temporary failures (no network, gateway down) retry after five minutes.
  • A permanent failure (revoked link, email outside the allowed domains) stops until the profile changes.
  • Later logins reuse a healthy enrollment, repair an expired one, and move the Mac to a rotated profile.
  • If ~/.anyray is deleted, the next key refresh re-enrolls without waiting for a login.
  • A full local anyray-connect --revert opts that account out until Connect is applied again.
  • A scoped --tools <id> --revert does not opt out. The console tool policy decides what this run applies, so the next managed enrollment re-applies that tool if the admin still enables it. See the Policy sync note under Fleet surfaces.
If your MDM cannot fill the email

Leave EnrollmentEmail empty, as with Jamf Now. The Mac enrolls as <local username>@<first allowed domain>. With no allowed domain and no email, enrollment stops and the Mac reports the failure.

What is bound to each Mac

Connect generates a key for the signed-in user, sends the managed email and the enrollment link to Anyray, and receives a DevCert with source provisioning binding the email and deployment to that key. Requests use the Mac's short-lived ark_… key. The certificate renews on its own while the Mac is in use, also under SSO, so no end-user sign-in is ever needed. The gateway does not call the MDM or fleetd.

Remove or rotate the profile

Rotating the provisioning token returns a new one-time profile with a new enrollment link. Deploy the new profile, then remove the old one. Macs move to the new link at their next login.

Rotation stops new enrollment with the old link, and removing the profile removes its enrollment link. Neither action revokes a DevCert or ark_… key already issued. To offboard a person, revoke the user, then the live key.

Windows and Linux

The Windows installer verifies the binary, installs it in the user's profile, registers a per-user scheduled task, and invokes managed enrollment once. The Linux package installs /usr/bin/anyray-connect, a systemd user service, and an XDG autostart entry.

When moving a fleet to the desktop app, verify app ownership and login state for every assigned user before retiring the CLI package's system-wide enrollment bootstrap. On macOS, IT unloads ai.anyray.connect.managed-enroll from each affected login session, then removes /Library/LaunchAgents/ai.anyray.connect.managed-enroll.plist through MDM. The per-user app does not elevate itself to remove a system-wide job. Keep the bootstrap on machines that still need CLI-only enrollment. This is separate from the old tray job, ai.anyray.connect-tray, which the desktop app migrates itself.

Linux desktop packages replace the anyray-connect CLI package while retaining /usr/bin/anyray-connect. The package transaction removes the CLI package's global managed-enrollment service and autostart file. Deploy the desktop to all assigned users of that machine before making this replacement.

The logon runner is silent and idempotent. Each logon reuses a healthy key, repairs an expired one, and re-binds the machine after a changed EnrollmentEmail or rotated link. Failures retry at the next logon. The runner never prints the enrollment link.

Email fallback

When the MDM cannot substitute a usable email, Connect falls back to <local username>@<first allowed domain>, minus any leading DOMAIN\ prefix on Windows. With no allowed domain, enrollment stops and retries at the next trigger.

Rejected policy files (Linux)

Connect ignores policy files that are not regular, are not root-owned, are group or other-readable, or exceed 64 KiB.

anyray-connect doctor --json reports a rejected local policy as enrollment.managedPreference: "invalid". The adjacent reason code is one of unparsable, malformed-url, untrusted-file, or unreadable; it never includes the link, email, or file path.

Provisioning tokens

allowedDomains holds one domain from the Email domains card; omit it to use the first. Domains match exactly: sub.customer.example.com does not satisfy customer.example.com.

Machine mode for shared or headless machines

Mint the token with Mode machine. Connect derives a stable machine id and records the user as machine:<id>. Set a default team at mint so spend rolls up. Use --machine in place of --user:

macOS / Linux
curl -fsSL https://app.anyray.ai/connect.sh | sh -s -- --enroll https://app.anyray.ai/enroll/enp_synthetic_example --machine --yes

A machine:… certificate keeps its fixed 1-year lifetime, so your MDM re-enrolls the device unattended when it lapses. Machine-mode tokens are blocked once SSO is active.

Enroll the machine too with --endpoint

An enrollment command configures tools only. Adding --endpoint also installs the endpoint agent and brings the machine under policy, on macOS, Linux, and Windows. The console's MDM command never carries the flag, so append it yourself. --no-endpoint overrides it.

The agent package is shared across organizations and fetches the org-specific fleet URL and enroll secret after client-key authentication. On macOS and Linux the command raises one sudo prompt itself. Windows has no equivalent: run it from a PowerShell session started with Run as Administrator, or it configures tool routing and leaves the machine unenrolled.

For locked-down fleets where developers cannot install privileged software, push the shared agent package and the organization's enrollment profile through MDM.

Claude Desktop is never configured by a bulk run

Configuring Desktop switches the app to third-party inference, which signs it out of claude.ai. The admin console stops delivering connectors, plugins, and model policy to that machine. Connect never takes that trade from a sweep: --yes configures every other detected tool and names Desktop in the output. Add --tools claude-desktop when you do want it, and reverse it per machine with anyray-connect desktop disconnect. See Claude Desktop.

Carry local Claude Desktop configuration

When an MDM policy does configure Claude Desktop for third-party inference, Connect copies local MCP servers, user-created skills, and user-installed plugins into the third-party profile on its own. MCP servers copy on the next reconcile; skills and plugins copy the next time Claude Desktop is fully quit, because the app rewrites those stores on exit. It never changes the first-party profile, and an entry already in the third-party profile wins. anyray-connect doctor shows the result under Claude Desktop profile; the states are listed in the Connect reference.

Chats from the claude.ai account are not local profile data. Export them from claude.ai, then run anyray-connect migrate --export <file.zip> after Claude Desktop is fully quit.

Legacy MDM tokens

The older no-sign-in lane, verified by endpoint evidence. New rollouts use the macOS package path instead. Existing MDM-kind tokens without proofMode: managed-preference keep their behavior: they enroll with no sign-in, SSO on or off, require email identity mode, and are trusted only after the endpoint agent proves the device is enrolled in a registered MDM server.

Managed-device evidence today covers macOS fleets under Jamf. Both evidence lanes read the same policy from the endpoint pack, Anyray - enrolled in the expected MDM. The injected EnrollmentEmail is MDM-supplied attribution, not a verified identity. The agent proves the device is managed, not who is at the keyboard.

Set up the legacy lane
1
Register the MDM server

On the Users page, open the MDM enrollment card. Add each MDM server under Registered MDM servers, one bare HTTPS URL per line, then save:

Registered MDM server
https://mdm.customer.example.com

An empty list blocks this lane. Saving a changed list re-templates the policy on the active lane. Hosts report the new result on their next policy run, about ten seconds for the local service, so a fresh registration can refuse briefly with mdm_not_verified.

2
Mint an MDM-kind token

The console no longer mints this token. Call post /admin/provisioning-tokens with kind: "mdm" and identityMode: "email". Copy the origin-qualified enp_synthetic… URL from the response, because the raw token is not recoverable later. Rotate with post /admin/provisioning-tokens/:id/rotate. A refused rotation leaves the current token intact.

3
Push the managed email

Deploy a custom profile to all target Macs that writes the managed preference domain ai.anyray.connect, key EnrollmentEmail, with the literal Jamf variable $EMAIL. Jamf replaces that value from the computer's User and Location record.

For Intune, write the same value as a machine policy:

Intune managed enrollment email
$keyPath = 'HKLM:\SOFTWARE\Policies\Anyray\Connect'
New-Item -Path $keyPath -Force | Out-Null
Set-ItemProperty -Path $keyPath -Name 'EnrollmentEmail' `
-Value 'dev@example.com' -Type String

Connect checks the per-user macOS managed preference first, then the device-wide one. On Windows it reads HKLM\SOFTWARE\Policies\Anyray\Connect\EnrollmentEmail. The managed email is honoured only for an MDM-kind token, and it then wins over --user. The team comes from the token's default team.

4
Create Jamf Policy A for the agent

Target All Managed Macs. Install the shared Anyray endpoint-agent package and deploy the organization's endpoint enrollment profile before the enrollment command runs. Scope the Jamf email profile to the same Macs. The agent must report Anyray - enrolled in the expected MDM before the gateway accepts the machine.

5
Create Jamf Policy B for the user enrollment

Create a Smart Group named agent installed and scope Policy B to it. Add both Enrollment Complete and Recurring Check-in triggers. Set execution frequency to Once per user per computer. Run the enrollment command as the signed-in console user, with no --user flag:

Jamf Policy B script
console_user=$(/usr/bin/stat -f %Su /dev/console)
if [ -n "$console_user" ] && [ "$console_user" != "root" ]; then
/usr/bin/sudo -u "$console_user" -H /bin/sh -c '
cd "$HOME" || exit 1
curl -fsSL https://app.anyray.ai/connect.sh | sh -s -- \
--enroll https://app.anyray.ai/enroll/enp_synthetic_example --yes
'
fi

Connect reads EnrollmentEmail, enrolls with source: mdm, and configures the tools selected by policy. No browser sign-in occurs.

What the gateway verifies

/connect/verify and later /connect/key/heartbeat requests carry the machine UUID beside the signed proof, which the gateway hashes before storage. It then requires all of:

  • An MDM server URL registered in the console.
  • The machine UUID joining to a reporting endpoint agent.
  • The pack policy Anyray - enrolled in the expected MDM passing on that host.
  • The endpoint service reachable, with evidence current enough to use.

A refusal returns managed_device_required with one closed reason:

ReasonMeaning
no_mdm_server_registeredThe console has no registered MDM server URL.
agent_missingThe machine UUID does not join to an endpoint-agent host, or that host has not reported for over 7 days.
mdm_not_verifiedThe MDM policy is failing, or has no result yet, on the host.
endpoint_unreachableThe evidence lane cannot be read safely, or has not yet converged on the current registered MDM server list.

Each refusal lands in the audit log (anyray_audit_log, category client-keys, action mdm-device-refused) with the user, the reason, and the lane. Metadata only.

The gateway repeats this check on every key heartbeat, and every refusal is audited. Only mdm_not_verified revokes the key. The other three leave it in place for the next heartbeat.

MDM retirement is not the immediate access switch

The shipped code does not force a heartbeat every 30 minutes for every MDM provisioning key. That scheduler serves short-lived keys and a small set of seats with no other refresh trigger. Default provisioning keys are long-lived, and the endpoint-agent freshness policy runs its refresh remediation only near key expiry.

Use SCIM deactivation for an immediate gateway block. For console-managed offboarding, remove the person, revoke the live key, then retire the Mac in MDM.

An optional Jamf offboarding policy can run anyray-connect --revert as the console user to remove local tool configuration and the stored enrollment. That is endpoint cleanup. Gateway-side revocation remains the access control.

Fleet surfaces

Run the enrollment command in the signed-in user's context. One push per surface.

SurfacePushNotes
Claude CodeRouted by enrollment.An API-key fleet can also pin routing with managed settings.
Codex app / CLI / IDEEnroll as the signed-in user.API-key and ChatGPT-seat routing are automatic. If the model picker omits custom models, set ANYRAY_CODEX_MODEL and rerun Connect. For managed API-key auth, deploy the desktop helpers.
Claude DesktopThe console-generated org policy .mobileconfig plus the helpers.Third-party inference ends claude.ai admin-console delivery. Connector grants are not exportable, so users re-authorize after deployment. To keep the app first-party, push a first-party profile.
JetBrainsThe acp.json artifact (below).Needs IDE 2025.3.2+ with JetBrains AI 253.30387.147+. Versions below the floor are left unchanged. ACP traffic routes automatically. Native JetBrains AI and Copilot traffic does not.
Devin DesktopEnroll as the signed-in user, then enable the ACP agents once in Devin and restart.Native usage is visible only through the Devin connector.
VS Code Copilot ChatEnroll as the signed-in user. Developers select an Anyray · … model.The key goes to VS Code SecretStorage. Native traffic stays on GitHub. The extension installs into every profile registered at apply time. A later profile needs a re-run.
Cursoranyray-connect <gateway> --tools cursor --user <email> --yes, then restart once for MCP.Native Team Claude/Opus stays the default, and no provider key is added. Native inference stays on Cursor with no gateway trace. See Cursor.
OpenCodeRouted by enrollment, including an install that happens later.Its Anthropic and OpenAI providers take the org key; a Copilot seat routes through the seat lane. A ChatGPT Pro/Plus sign-in inside OpenCode cannot be routed: its endpoint is hard-coded, so those turns never reach the gateway.

ACP surfaces (JetBrains, Devin) need Node, and have no WSL support. For Remote Development, run Connect as the remote backend user.

For Devin Enterprise and GitHub Copilot Business/Enterprise reporting, configure subscription spend connectors once on the gateway.

Policy sync

The values emitted by anyray-connect managed bootstrap enrollment and older clients. Enrolled clients treat /connect/policy as the live source for routing origin, team attribution, enabled tools, and shared skills. sync and the detached refresh worker repair drift automatically.

Tool reconciliation is enable-side authoritative with revert tombstones. An enabled id the machine never ran enrolls fleet-wide. A developer's intentional revert sticks, and only the locked tier overrides the tombstone. An omitted id is never reverted or removed.

On a managed Mac, the periodic managed-enrollment run reads the same /connect/policy tool list, and it, not the developer, decides what this run configures. It applies every tool the policy enables and reverts a tool the policy no longer enables, even if the Mac still has it configured. A developer's local revert never blocks this: the tombstone is not consulted, so an admin-enabled tool always lands on the next managed enrollment. This run's applied NDJSON event lists every tool it skipped or reverted, with the reason, so a dropped tool always has a line explaining why.

Claude Desktop and Codex fleet policies

Owned by Desktop apps: managed fleets and the desktop command reference. In short: deploy a stable /usr/local/bin/anyray-connect, install both helpers with desktop helper --write, then emit policy with desktop fleet static (Claude Desktop) or desktop codex --mobileconfig (Codex), passing the same fleet-stable helper path. Neither wrapper holds a key.

JetBrains and shell artifacts

Credentialless managed --print artifacts, explicit flags only. Stdout is the artifact.

JetBrains acp.json (per-user file: ~/.jetbrains/acp.json)
anyray-connect managed --print --tool jetbrains \
--bin /usr/local/bin/anyray-connect --npx /usr/local/bin/npx > acp.json

Entries launch Connect's ACP agents at the fleet-stable paths. Gateway, attribution, and the personal key resolve from each user's own enrollment at agent launch, so the file carries none of them. An MDM file drop replaces the whole file, so merge the agent_servers object in your config tool if developers add their own agents.

Shell / SDK snippet (deploy to /etc/profile.d/anyray.sh)
anyray-connect managed --print --tool shell --gateway https://customer.example.com > anyray.sh

The snippet exports the two gateway base URLs and nothing else, so a shared file never carries a credential or shadows a seat sign-in.

Zero-touch bootstrap for ACP and editor surfaces

A developer who never ran Connect still needs one enrollment. Deploy the fleet bootstrap file, and the ACP launcher starts enrollment for them.

Fleet bootstrap (SSO link + gateway origin + binary path)
anyray-connect managed --print --tool bootstrap \
--sso https://app.anyray.ai/sso/tnt_synthetic --gateway https://customer.example.com \
--bin /usr/local/bin/anyray-connect > bootstrap.json

--bin is optional and records where your MDM installed Connect, for trigger surfaces that are not the binary itself. Today the VS Code extension uses it.

The bin path is a spawn target, so lock down the file's ownership

Deploy the bootstrap file admin or root-owned, never user-writable. On Windows, --bin must point under Program Files, which is enforced at generation. %ProgramData% itself lets standard users create files, so have your MDM create %ProgramData%\Anyray\bootstrap.json as SYSTEM at provisioning time, before first login. An invalid bin value is ignored.

Deploy it to the fixed system path: /etc/anyray/bootstrap.json (Linux), /Library/Application Support/Anyray/bootstrap.json (macOS), or %ProgramData%\Anyray\bootstrap.json (Windows). Three surfaces read it: the ACP launcher, the prompt hook, and the VS Code extension. Each shows a sign-in message with a verification URL and code on first unenrolled use. Prompts are never blocked or altered, sign-in completes in the background, and the next use works. The file is configuration, not a credential, so world-readable is fine.

Shells and arbitrary SDKs have no trigger surface that can safely pause a request for sign-in. The managed shell snippet routes them to the gateway, but each user must enroll once first. For a non-interactive fleet job, use that job's secret-management system instead.

Managed settings for Claude Code

Claude Code reads a system-path managed-settings.json that ranks above user and project settings, so your MDM can pin gateway routing fleet-wide. anyray-connect managed emits or installs that file with no credential.

This artifact is for API-key fleets. The file owns routing and headers, while per-user ark_ keys come from provisioning-token enrollment and stay in user scope. For a subscription fleet an env file is rejected. --sso <link> emits a hooks-only file that configures Claude Code only, which is the fix for a policy that sets allowManagedHooksOnly (troubleshooting), not a fleet enrollment. Enroll seat fleets through an MDM lane.

OSPath
macOS/Library/Application Support/ClaudeCode/managed-settings.json
Linux / WSL/etc/claude-code/managed-settings.json
WindowsC:\Program Files\ClaudeCode\managed-settings.json

Claude Code before v2.1.75 uses the legacy Windows path C:\ProgramData\ClaudeCode\managed-settings.json.

API-key fleet
anyray-connect managed --print --gateway https://customer.example.com --team eng > managed-settings.json

managed --print writes JSON to stdout for a Jamf profile, Intune policy, or Ansible template, with notes on stderr. On a single host, sudo anyray-connect managed --write … merges into any existing file and backs up the original. managed --revert removes only the Anyray-owned keys. The file loads at Claude Code start, so restart open sessions.

Packaging from PowerShell needs --out

Add --out <path> to write the file directly, UTF-8 with no BOM. Windows PowerShell 5.1 writes UTF-16LE for >, and -Encoding utf8 adds a BOM, so no JSON reader accepts the redirect.

The connect.ps1 installer ends by propagating Connect's exit code, which unwinds out of the scriptblock and closes the window. A form that captures the output and writes it on a second statement never writes anything. --out writes within the run.

Prefer --team over --user, because a static header cannot vary per machine. --intent, --intent-label, and --session are also accepted.

When an existing managed file conflicts

If the managed file already carries an ANTHROPIC_BASE_URL from other tooling, it outranks whatever per-user enrollment writes. anyray-connect then reports success on every machine while the traffic goes to the older endpoint.

anyray-connect doctor flags the Connect-owned keys that disagree with the managed file, naming the key, the path, and the origin requests actually reach. Agreement is silent. No re-run and no doctor --repair can outrank managed scope, so the fix is always to change the file.

A machine that never enrolled has nothing in user scope for the comparison, so a managed base URL there reads as a plain unrouted tool.

forceLoginMethod conflicts

Claude Code documents that forceLoginMethod and forceLoginOrgUUID cannot be combined with gateway credentials from v2.1.146 onward. A fleet that pins Claude Code sign-in that way has to resolve it first. The seat lane is the likely exception, because Claude Code keeps its own sign-in there and the personal key rides a separate header. Verify on one machine.

The file beats user settings, not the shell

Whether its env block overrides an exported shell variable such as ANTHROPIC_BASE_URL in ~/.zshrc is undocumented by Claude Code. Verify routing on one machine before a fleet rollout.

Offboard a machine-mode device

User revocation takes an email, so it covers email identity mode only. A device enrolled in machine mode is bound to machine:…, which the revocation list cannot hold. Revoke its live key (delete /admin/client-keys/:id) for the cutoff, and revoke the provisioning token so it cannot re-enroll. Its certificate is not rolled forward, so it also lapses at its fixed 1-year expiry.