Claude Desktop (MDM)
Claude Desktop (including Cowork) can send inference to Corveil through Developer → Configure Third-Party Inference → Gateway. An MDM profile locks that choice to your Corveil host. People on managed devices do not open People & Access → API Keys, and they do not paste a token.
This page is the fleet setup. The Claude Cowork page is the manual, per-machine passthrough variant, and it uses an older config shape. The Claude Desktop page is an older environment-variable setup that still requires a pasted key.
Build the profile in the app on one unmanaged admin machine (Help → Troubleshooting → Enable Developer Mode, then Developer → Configure Third-Party Inference…), test the connection, and export it. Anthropic's Deploy with MDM and gateway pages are the export and encoding reference. The keys below are the ones that matter for Corveil.
What the managed profile locks
Claude Desktop reads a managed configuration from the operating system. When that source sets the inference keys, it wins: values the user writes locally are ignored, and the in-app configuration window opens read-only. The app applies the profile at launch (and, from Desktop 1.46388.1, on its next configuration re-check). Push the profile before the installer so the first launch is already the third-party deployment.
| Platform | Managed location |
|---|---|
| macOS | /Library/Managed Preferences/<user>/com.anthropic.claudefordesktop.plist (per-user; wins over the machine file /Library/Managed Preferences/com.anthropic.claudefordesktop.plist) |
| Windows | HKLM\SOFTWARE\Policies\Claude |
| Linux | /etc/claude-desktop/managed-settings.json |
A few encoding rules, from Anthropic's current reference:
- Windows. Machine policy and user policy are separate. When any value exists directly under
HKLM\SOFTWARE\Policies\Claude, the app ignoresHKCU\SOFTWARE\Policies\Claude. Put the whole profile in one hive. The in-app export writes user policy,HKCU\SOFTWARE\Policies\Claude, with values directly under that key. Booleans come out asDWORD(chatTabEnabled=dword:00000001). Object-typed keys such asbannercome out as oneREG_SZJSON string.REG_SZis also accepted for booleans and integers. - Linux. The file is native JSON, owned by root, not a symlink, and not group- or world-writable.
/etc/claude-desktophas the same ownership rules. A file that fails those checks is rejected and local settings are disabled until it is fixed. - macOS and Windows store object-typed keys (such as
inferenceGatewayOidc) as one JSON string, not as nested keys. Export from the app rather than hand-nesting them.
A profile that sets only the app-behavior keys (auto-update, relaunch window, configuration re-check, network proxy) does not lock inference. To pin Gateway mode, the profile has to set inferenceProvider and the gateway fields below. Also set disableDeploymentModeChooser to true so the device cannot switch back to a claude.ai sign-in.
Set inferenceGatewayBaseUrl to the Corveil origin, with no /v1 suffix — https://corveil.io for the hosted gateway. Desktop calls GET /v1/models and POST /v1/messages on that origin. Confirm with the in-app connection test on one device before the fleet push. Which of those models Cowork keeps is covered under Models Cowork will show.
Managed-key and passthrough
Gateway mode has two routing choices, and they are not interchangeable. Pick one for the fleet. Spend limits count traffic in both. Passthrough keeps billing on the customer's Anthropic contract. A static sk-citadel-… bearer is managed-key mode and does not preserve subscription billing.
| Managed-key | Passthrough | |
|---|---|---|
| Guardrails and request logs | Yes | Yes |
| Budgets | Yes. Spend limits count this traffic. | Yes. Spend limits count this traffic. |
| Who is billed upstream | Corveil's organization Anthropic credential | The user's own Claude / Anthropic contract |
Who Desktop signs in as is a separate choice. Use one of the next two sections. When inferenceCredentialKind is helper-script, Desktop uses only that helper.
Signing in to Claude (claude.ai, Claude Desktop, or Claude Code) is not a Corveil credential. That login, including an Anthropic SSO login, produces the user's Claude OAuth token. On macOS, Claude Code stores it in the Claude Code-credentials keychain entry. The passthrough helper below prints that token. In that profile it is the upstream Anthropic credential. It does not satisfy managed-key mode.
Single sign-on
This is the fleet sign-in. Set inferenceCredentialKind to interactive and set inferenceGatewayOidc. Needs Claude Desktop 1.6889.0 or later. No helper runs, and no long-lived gateway key is stored on the device.
This sign-in does not mint an sk-citadel- secret. Desktop sends the WorkOS access token as the Corveil bearer on /v1, in managed-key and in passthrough. The token identifies the member. It is not copied onto the Anthropic request, and it is not an upstream Anthropic credential. A claude.ai or Anthropic SSO login is a different sign-in and is not this credential.
| Managed-key | Passthrough | |
|---|---|---|
| What Desktop sends as the Corveil bearer | The WorkOS access token, with inferenceGatewayAuthScheme bearer (the default) | The WorkOS access token |
| What is forwarded to Anthropic | Corveil's organization Anthropic credential | The Anthropic key already on the machine |
Before that profile can authenticate, an organization admin registers the issuer. POST /api/organizations/{orgID}/inference-oidc with the issuer URL, the audience (the Desktop client id), and bearer_type (access_token or id_token). Optional jwks_url overrides discovery. The dashboard has no control for this list, and the gateway does not create a row on its own. A token whose issuer is missing from the list is rejected.
The Corveil SSO profile sends the WorkOS access token, so that row names the WorkOS issuer and bearer_type is access_token. The same list accepts any other OpenID Connect issuer. A Desktop profile whose inferenceGatewayOidc block points at Entra or Okta sends that provider's token, and the registered row has to name that issuer.
interactive without inferenceGatewayOidc is a different mode. Anthropic uses it when the gateway publishes OAuth metadata at /.well-known/oauth-authorization-server. Corveil publishes that document for MCP clients. Those MCP tokens are not inference credentials: /v1 rejects them. This section's profile sets both inferenceCredentialKind: interactive and inferenceGatewayOidc.
Cowork does not forward a Claude subscription OAuth token the way the Claude Code CLI can. On this passthrough profile, Desktop forwards the Anthropic key already on the machine, and the WorkOS access token stays the Corveil bearer. Managed-key calls Anthropic with Corveil's organization credential.
Virtual-key helper
Corveil does not ship a Desktop login program. corveil login is the CLI, and it is not a credential helper you can point Desktop at. This section is for a fleet that places an sk-citadel- secret on the device. A managed fleet that should not do that uses Single sign-on.
Signing in to the Corveil web app does not hand MDM a key. The dashboard Default API key is still mint-once. The first web-app sign-in mints one key with that name. The server sees the secret only inside that mint, uses it to write an audit row, and does not return it to the browser. Later logins find the same row and get an empty secret, because Corveil stores a hash. Directory sync can create the same key record, and it discards the secret the same way. Revoking Default API key does not mint a replacement; the row has to be deleted before a new one can be created. Do not build a profile, a script, or a helper that tries to re-read Default API key.
corveil login is the interactive login that returns a Corveil key. It opens a browser to GET /auth/cli-login (your organization's Corveil SSO), then POST /auth/cli/exchange mints a new virtual key and returns sk-citadel-… once. The key is named CLI on <hostname>. The plaintext is not shown again. A later login from the same hostname deactivates that key and mints a replacement; it does not reveal the old secret.
corveil login --url https://corveil.ioThat command is the right way for an administrator, or a single user at a terminal, to mint a key without using the dashboard. It always sends the machine hostname, it writes the key into the CLI's config profile, and it does not print a bare token on stdout. Pointing inferenceCredentialHelper at corveil login will not work, and a Desktop install that shared the machine hostname would deactivate the CLI key (and the reverse).
A virtual-key profile can use inferenceCredentialKind: helper-script when you install your own executable. On first launch that program runs the /auth/cli-login + /auth/cli/exchange handshake, caches the new key, and prints it. The user signs in through the browser. They never copy a token. Do not combine the helper with interactive. When the kind is set, Desktop uses only that source.
Illustrative managed-key profile (logical keys; export to plist or registry from the app). The helper path is a placeholder for the executable you install:
{
"inferenceProvider": "gateway",
"inferenceGatewayBaseUrl": "https://corveil.io",
"inferenceGatewayAuthScheme": "bearer",
"inferenceCredentialKind": "helper-script",
"inferenceCredentialHelper": "/usr/local/bin/mint-gateway-key",
"inferenceCredentialHelperWindows": "C:\\Program Files\\YourOrg\\mint-gateway-key.exe",
"inferenceCredentialHelperTtlSec": 3600,
"inferenceCredentialHelperSilentRefreshEnabled": false,
"inferenceCredentialHelperTimeoutSec": 300,
"modelDiscoveryEnabled": true,
"disableDeploymentModeChooser": true
}The helper's contract:
- Stdout is only the credential. Either a bare
sk-citadel-…token, or one JSON object{"token":"sk-citadel-…"}. Banners and logs go to stderr. Exit 0 on success. CLAUDE_HELPER_CONTEXT=interactive(first run, and Desktop's "Sign in again" after a rejected credential) is the only time the helper may call the exchange. Bind127.0.0.1on a port from 1024 through 65535, open{origin}/auth/cli-login?cb_port=<port>&state=<nonce>, and on the loopback redirect immediatelyPOST /auth/cli/exchangewith{"token":"<exchange>","hostname":"<name>"}. The exchange token lives for about a minute and is single-use. Readapi_keyfrom the JSON response, replace the cache, and print it.- Every other context (
mid-session-refresh,background,scheduled-task,setup-test, and the TTL re-run) reprints the cached key, or exits non-zero if the cache is missing. Do not call/auth/cli/exchangeon those runs. A repeat exchange with the same hostname deactivates the key Desktop is already using. - Hostname. Send a name that is unique per device and different from the machine hostname
corveil loginuses, for exampleclaude-desktop-<machine-hostname>. The server truncates it at 64 characters and names the keyCLI on <that name>. One shared literal for the whole fleet makes every desktop rotate the same key. - Cache. Store the plaintext in the user keychain (macOS Keychain, Windows Credential Manager, libsecret) or in a
0600file owned by the user. There is no refresh token to mint a new key without a browser. SetinferenceCredentialHelperSilentRefreshEnabledtofalseso a mid-session refresh does not expect a non-interactive re-login. The TTL re-run still happens; the reprint branch is what serves it. RaiseinferenceCredentialHelperTimeoutSec(default 60, maximum 600) so the first browser sign-in fits. 300 seconds is a reasonable start. - Windows. The in-app registry export writes the helper path in
inferenceCredentialHelper. Anthropic's reference also acceptsinferenceCredentialHelperWindowsfor a Windows executable. Point whichever key you set at the helper installed on that PC.
A revoked key recovers when the user chooses Sign in again. That re-invokes the helper with interactive, which mints a replacement and replaces the cache. If a non-interactive run reprints a key you already know is bad, Desktop accepts the output and the next turn fails again with no prompt — exit non-zero instead.
Static key in the profile
inferenceCredentialKind: static plus inferenceGatewayApiKey also selects managed-key mode. Use it only when you can inject a distinct, freshly minted key per user. The plaintext still has to be captured at mint time (corveil login or the exchange above). The profile cannot be filled from Default API key after the fact. A single key shared by the whole fleet collapses per-user budgets and attribution. A static key is a long-lived secret in the managed profile.
Passthrough helper
This is the profile that Cowork accepted. It is the flat form of the manual Claude Cowork JSON. The macOS .mobileconfig and the Windows .reg export carry the same keys. inferenceCustomHeaders is absent. Desktop treats that map as plain configuration, drops a string value, and then sends no custom headers. The virtual key lives only in the helper.
{
"inferenceProvider": "gateway",
"inferenceGatewayBaseUrl": "https://corveil.io",
"inferenceCredentialKind": "helper-script",
"inferenceCredentialHelper": "/usr/local/bin/claudecode.sh",
"chatTabEnabled": true
}On macOS the export is a user-scoped configuration profile whose payload type is com.anthropic.claudefordesktop. chatTabEnabled is a boolean. On Windows the same export is HKCU\SOFTWARE\Policies\Claude, and chatTabEnabled is a DWORD. A banner object, when set, is one JSON string on both. A macOS export stores a macOS path in inferenceCredentialHelper. On Windows, set that value to the helper installed on the PC. Anthropic's reference also accepts inferenceCredentialHelperWindows for that path.
Stdout is one JSON object. token is the Claude Code OAuth access token from the Claude Code-credentials keychain entry (it starts with sk-ant-oat01). headers carries the Corveil virtual key:
{"token":"<claude-oauth-access-token>","headers":{"x-citadel-api-key":"sk-citadel-your-key-here"}}#!/bin/sh
token=$(security find-generic-password -s "Claude Code-credentials" -w | jq -r '.claudeAiOauth.accessToken')
jq -nc --arg token "$token" --arg key "sk-citadel-your-key-here" \
'{token:$token, headers:{"x-citadel-api-key":$key}}'The key is a real sk-citadel- value minted ahead of time (corveil login or People & Access → API Keys). It is ASCII only. A literal ellipsis character in sk-citadel-… fails the connection test while Desktop is still building the header (ByteString, code point 8230 at index 11). The script prints that JSON and nothing else. It does not emit ANTHROPIC_CUSTOM_HEADERS, and it does not refresh an expired Claude Code access token. Re-login to Claude Code, then fully quit Cowork. Helper output is cached for inferenceCredentialHelperTtlSec (default 3600). Spend limits count this traffic.
Models Cowork will show
Cowork fills the picker from GET /v1/models. It keeps a row only when the model id contains claude, anthropic, sonnet, opus, haiku, fable, or mythos. Kimi, Gemini, and other non-Claude catalog ids are dropped. The log line is Gateway /v1/models returned 0 usable models with a non-zero rawCount, and the connection test reports an empty picker.
Enable a Claude model for the organization under AI Gateway → Providers → Catalog. Enabling claude-opus-4-8 made that id the one usable model in a list that also contained Kimi and Gemini. Run the connection test again after the catalog change. Cowork keeps the cached discovery result until the next test or relaunch. On a virtual-key helper, that key has to belong to the organization. On single sign-on, the registered issuer has to belong to it.