Skip to main content

MDM reference

Depth behind Bulk enrollment with your MDM.

SCIM or MDM​

The two solve different halves of onboarding managed devices.

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 deactivation of existing keys within a second.
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, revoked user, 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, or Disconnect in the desktop app, opts that account out until Connect is applied again. The app then shows a single Reconnect screen. Reconnect ends the opt-out and enrolls the Mac again with its managed email. Required tools come back on their own; the person switches the rest back on in Tools.
  • 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 Tools on managed devices.
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.

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.

Install the Anyray Connect desktop pkg. On every Mac, its preinstall removes the package-installed Connect CLI by receipt. There is no option to keep the CLI bootstrap. A Homebrew or npm copy at /usr/local/bin/anyray-connect makes the install refuse before writing and names that path. A foreign credential or bootstrap helper does the same. The installer also unloads and removes the system-wide managed-enroll LaunchAgent, so retire any MDM payload that still delivers it.

When someone is signed in, the pkg opens the app for them as the install finishes. On a Mac at the login window or in Setup Assistant, the enrollment profile opens it at the next login: the profile carries a managed login item for /Applications/Anyray Connect.app, for every user on the Mac. A profile created before the login item was added has none. Rotate it to get one.

The sweep also removes the retired Anyray endpoint agent (fleetd) and leaves a customer's own Fleet agent in place. It keeps fleetd's osquery logs (/opt/orbit/osquery_log and /var/log/orbit) as audit evidence. anyray-connect doctor lists them for information and does not count them as a problem. Remove both as root once you no longer need them. A locked file does not stop the install. The leftover appears in anyray-connect doctor and the machine residue report.

For a managed uninstall, first take the Mac out of scope of the desktop pkg and of the enrollment profile. While either is still assigned, the MDM reinstalls the app and it enrolls again without asking. Then run the installed script as root, once per Mac:

Managed uninstall command
/usr/local/lib/anyray-connect/uninstall.sh

It runs the Connect user-layer uninstall for every user with Anyray state, then removes the app, the /usr/local/bin launcher and helpers, and the package receipt. Running it again on a Mac that is already clean changes nothing. With nobody signed in, the login item cannot be unregistered, and macOS lists a dead entry until the user opens Login Items. An MDM that can only deploy packages deploys Uninstall Anyray Connect.pkg from the same directory instead; it runs the same script. The take-out-of-scope step applies to Windows and Linux too.

Windows and Linux​

Intune desktop install​

The tray starts once for each user at their first sign-in after the install.

Deploy the signed Anyray Connect MSI in the system context. It installs the engine and tray under C:\Program Files\Anyray, adds that directory to the machine PATH, and regenerates the two Claude Desktop and Codex helper wrappers. The install removes the retired endpoint agent (Fleet Orbit) through Windows Installer, then removes its remaining C:\Program Files\Orbit runtime state. It does not change any user's tool configuration or %APPDATA%\Anyray state. Where MDM isn't available, connect.ps1 remains the per-user CLI installer: it installs the engine under the signed-in user's profile, registers a per-user scheduled task, and invokes managed enrollment once.

If a foreign, non-Anyray .cmd file already occupies a shim path (anyray-credential-helper.cmd or anyray-bootstrap-headers-helper.cmd), the install fails with Desktop helper regeneration failed; remove that file and re-run the install. A foreign, unsigned anyray-connect.exe at the install path fails the same way before any file is copied, instead of being silently overwritten.

Before copying new files, the install moves the previous signed engine, managed shims, and leftover Orbit runtime state into a staging directory under %ProgramData%\Anyray\install-rollback. If a later install step fails, Windows Installer rolls the moved files back into place; once the install finishes successfully, the staging directory is deleted. Orbit's own package removal through RemoveExistingProducts is a Windows Installer built-in and cannot be rolled back this way.

For a managed uninstall, run the installed script as SYSTEM:

Intune uninstall command
powershell.exe -NoProfile -ExecutionPolicy Bypass `
-File "C:\Program Files\Anyray\uninstall.ps1"

The script runs the Connect user-layer uninstall for each loaded profile through a one-shot interactive scheduled task, then removes the MSI with msiexec /x. On an engine that predates the uninstall verb, the task runs offboard instead: routing and enrollment are removed, but the login item and %APPDATA%\Anyray stay until the engine is updated. Profiles that are not logged in keep their per-user state. Removing the app from Settings → Apps also removes only the machine layer.

Linux package​

The Linux package installs /usr/bin/anyray-connect, a systemd user service, and an XDG autostart entry.

Linux desktop packages replace the anyray-connect CLI package and the deprecated fleet-osquery package while retaining /usr/bin/anyray-connect. The package transaction removes the CLI package's global managed-enrollment service and autostart file, stops orbit.service, and removes /opt/orbit and /var/log/orbit. On Debian and Ubuntu the install fails if a legacy package or the orbit service is still present. rpm runs its checks before the old package is erased, so on Fedora and RHEL the install instead asserts that the new package owns /usr/bin/anyray-connect and that orbit.service is stopped and disabled. Deploy the desktop to all assigned users of that machine before making this replacement.

apt remove or dnf remove only removes this machine-level state. Per-user autostart entries and tool configs stay behind. Run the package's /usr/lib/anyray-connect/uninstall.sh as root to also remove every enrolled user's state before removing the package. On an engine that predates the uninstall verb, the script instead runs offboard for each user: routing and enrollment are removed, but the login item and ~/.anyray stay until the engine is updated.

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

Windows policy sets only EnrollmentUrl. Connect uses the signed-in user's UPN when its domain is allowed. Otherwise it uses <local username>@<first allowed domain>, minus any leading DOMAIN\ prefix. Linux JSON does not expand $EMAIL; replace it during deployment or leave it for the same username fallback. 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 lists domains from the Email domains card; omit it to use the first. The console sends every card domain, with the Fallback domain first: Connect uses it for a device whose MDM sends no email. Domains match exactly: sub.customer.example.com does not satisfy customer.example.com.

Machine mode for shared or headless machines​

On the Headless (CLI) tab, set Enroll as to Machine id. 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.

A machine-mode seat cannot share or open shared pages. Both need a person's own key.

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 --with-tools claude-desktop when you do want it, and reverse it per machine with anyray-connect desktop disconnect. See Claude.ai organization management.

Carry local Claude Desktop configuration​

When an MDM policy routes Claude Desktop through Anyray, Connect copies local MCP servers, user-created skills, user-installed plugins, Claude Code sessions, and Cowork tasks into the third-party profile on its own. MCP servers copy on Connect's next background refresh; the rest 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. Each session copies once, so one deleted afterwards stays deleted. A session that could not be read or copied is retried on later runs, up to five times. If the policy stops routing Desktop through Anyray before the copy runs, the copy is dropped. Sessions from more than one Claude account wait until the person picks one with anyray-connect migrate --account <id>. 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​

An MDM-kind token without proofMode: managed-preference keeps working: its keys mint and renew with no endpoint check. Use the macOS package path for new enrollments.

Tools on managed devices​

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

SurfacePushNotes
Claude CodeRouted by enrollment.An API-key organization 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's anyray-claude-desktop.mobileconfig (steps).Third-party inference ends claude.ai admin-console delivery. Connector grants are not exportable, so users re-authorize after deployment. Claude lists and opens pages shared with the user through the managed anyray server. 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 provider takes the org key; a Copilot seat routes through the seat lane. A ChatGPT Pro/Plus sign-in routes through a plugin Connect writes. Where it cannot write that plugin (a plaintext http:// gateway, or another owner's file at its path) it also keeps the gateway key and attribution off the OpenAI provider, so those turns stay on ChatGPT with no Anyray credential attached. The OpenAI provider still takes the org key there, except on a lane pointed at your own upstream, which is left off the gateway instead. See OpenCode.

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 on every managed device. A developer's intentional revert sticks, and only a Required tool 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 holds for Available tools (and for every tool when no tool policy is saved); a Required tool lands again on the next managed enrollment. This run's applied NDJSON event lists every tool it skipped or reverted, with the reason.

Claude Desktop and Codex managed policies​

Claude Desktop: Claude Desktop on managed devices. Codex: deploy /usr/local/bin/anyray-connect, install both helpers with desktop helper --write, then emit the policy with desktop codex --mobileconfig, passing the helper path (desktop commands). 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 paths that are the same on every device. 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 bootstrap file, and the ACP launcher starts enrollment for them.

Bootstrap file (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 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 on every managed device. anyray-connect managed emits or installs that file with no credential.

This artifact is for API-key organizations. The file owns routing and headers, while per-user ark_ keys come from provisioning-token enrollment and stay in user scope. For subscription seats 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 an enrollment. Enroll subscription seats 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 organization
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. An organization 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 wider 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.