MDM reference
Depth behind Bulk enrollment with your MDM.
SCIM or MDM
The two solve different halves of onboarding managed devices.
| Mechanism | Source of truth | Credential | Best for |
|---|---|---|---|
| Inbound SCIM | Your 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 provisioning | Your 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.
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
~/.anyrayis 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> --revertdoes 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:
/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:
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
- Windows (PowerShell)
curl -fsSL https://app.anyray.ai/connect.sh | sh -s -- --enroll https://app.anyray.ai/enroll/enp_synthetic_example --machine --yes
& ([scriptblock]::Create((irm https://app.anyray.ai/connect.ps1))) "--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.
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.
| Surface | Push | Notes |
|---|---|---|
| Claude Code | Routed by enrollment. | An API-key organization can also pin routing with managed settings. |
| Codex app / CLI / IDE | Enroll 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 Desktop | The 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. |
| JetBrains | The 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 Desktop | Enroll 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 Chat | Enroll 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. |
| Cursor | anyray-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. |
| OpenCode | Routed 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.
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.
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.
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.
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.
| OS | Path |
|---|---|
| macOS | /Library/Application Support/ClaudeCode/managed-settings.json |
| Linux / WSL | /etc/claude-code/managed-settings.json |
| Windows | C:\Program Files\ClaudeCode\managed-settings.json |
Claude Code before v2.1.75 uses the legacy Windows path
C:\ProgramData\ClaudeCode\managed-settings.json.
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.
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.