> ## Documentation Index
> Fetch the complete documentation index at: https://www.truefoundry.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy AI clients with MDM

> Deploy models, MCP servers, and Skills to your organization's AI clients with one TOML configuration and your existing MDM.

Use your Mobile Device Management (MDM) system to connect employees' AI clients to the **TrueFoundry AI Gateway**. One administrator-controlled TOML file selects the clients, models, MCP servers, Skills, and authentication method for a rollout.

This guide starts with **manual PAT onboarding**: scheduled MDM runs do not open a browser. Employees sign in once from a terminal, and later MDM runs reuse their Personal Access Token (PAT). [OIDC and automatic device login](#other-authentication-methods) are alternatives.

The setup tool configures clients; it does not install the clients or the MDM scheduler. Some clients support protected machine settings, while others receive user settings that setup reapplies. See [supported clients](#supported-clients) for the difference.

## Before you start

You need:

* **Administrator deployment access** through Jamf, Mosyle, Kandji, Intune, SCCM, or another system that runs scripts as root or Administrator/SYSTEM.
* **Your control-plane URL, tenant name, and gateway URL.** The control-plane URL opens your TrueFoundry console; the gateway URL receives model requests. Copy the gateway base URL from your organization's [Playground code snippet](https://www.truefoundry.com/docs/ai-gateway/quick-start.md#gateway-base-url), including any path such as `/api/llm`.
* **Gateway model IDs** that employees can use. Configure model and MCP permissions through [Access Control](https://www.truefoundry.com/docs/ai-gateway/gateway-access-control.md). For Codex, use a suitable Virtual Model with a bare OpenAI model slug, such as `gpt-5.4`; see [Codex model configuration](https://www.truefoundry.com/docs/ai-gateway/openai-codex-cli.md#why-you-need-a-virtual-model).
* **Permission for employees to create PATs** for manual onboarding. If your tenant requires a team owner, decide whether to set `auth.pat_team` or let employees select a team during sign-in.
* **Optional MCP servers and Skills** already registered in TrueFoundry. Use the exact registry names and grant employees access. See [MCP servers](https://www.truefoundry.com/docs/ai-gateway/mcp/mcp-overview.md) and [Skills Registry](https://www.truefoundry.com/docs/ai-gateway/skills/skills-registry.md).

The examples pin **`tfy-local-ai-setup` v2.2.0**, which includes standalone `--config`, manual PAT onboarding, MCP discovery, and additional clients. Review the [release notes](https://github.com/truefoundry/tfy-local-ai-setup/releases/tag/v2.2.0) before changing the pin.

## How the rollout works

| Stage | What happens |
| - | - |
| IT deploys the configuration | The privileged run saves the organization policy and applies work that can proceed without credentials. Browser sign-in stays deferred. |
| The employee signs in | `tfy-local-ai-setup login --mdm` reads that policy, opens browser sign-in when needed, creates and securely saves a one-year PAT, and applies user-owned settings and Skills. |
| MDM runs again | The privileged run reuses the employee's PAT and completes administrator-owned settings that contain credentials. Later runs reconcile the policy. |
| The employee opens the clients | Clients use the configured gateway. MCP servers can require separate OAuth authorization inside each client. |

> [!NOTE]
>
> A successful first MDM run does not mean every client is ready. In manual PAT mode, credential-dependent configuration waits for employee sign-in. Claude Code, Codex, and Claude Desktop managed settings are completed by a subsequent privileged run. Pilot the entire sequence before deploying broadly.

## Deploy your first configuration

### 1. Prepare the TOML file

Save the following as `mdm.toml` in your MDM deployment package. Replace the URLs, tenant, and model IDs. This example configures Claude Code, Codex, and OpenCode; add `claude-desktop` to `tools` on macOS/Windows if required.

```toml mdm.toml theme={"dark"}
url = "https://acme.truefoundry.cloud"
tenant = "acme"
gateway = "https://gateway.example.com/api/llm"
tools = ["claude-code", "codex", "opencode"]

[auth]
mode = "api-key"
login = "manual"
# Optional: team that owns the employee's PAT.
# pat_team = "engineering"

[defaults]
models = ["gpt-5.4"]
default_model = "gpt-5.4"

[mcp]
servers = []

[skills]
names = []

[agents.claude-code]
opus_model = "claude-code/claude-opus"
sonnet_model = "claude-code/claude-sonnet"
haiku_model = "claude-code/claude-haiku"

[agents.codex]
model = "gpt-5.4"
```

`api-key` selects per-user authentication; it does **not** ask you to embed an API key in this file. Keep tokens and client secrets out of the TOML. The example starts without MCP servers or Skills so you can verify model access first, then [add them](#give-clients-different-mcp-servers-and-skills).

The explicit `tools` list configures those clients even if they are not installed yet. Omit a client's ID to leave it out of the rollout. For Linux, leave out Claude Desktop.

### 2. Prepare the Claude Code policy template

Save this as `base-settings.json` alongside `mdm.toml` in your deployment package. This starter policy requires approval before writes and pushes and denies reads of SSH files. Review it with your security team and adjust it for your organization.

```json base-settings.json theme={"dark"}
{
  "permissions": {
    "disableBypassPermissionsMode": "disable",
    "ask": ["Bash(git push:*)", "Write(**)"],
    "deny": ["Read(**/.ssh/**)"]
  },
  "allowManagedPermissionRulesOnly": true,
  "allowManagedHooksOnly": true,
  "transcriptRetentionDays": 14
}
```

The wrappers explicitly select this template. This avoids inheriting an existing machine policy or the binary's built-in sandbox proxy settings. Gateway, model, auth, and MCP values are merged by the binary. See [advanced client controls](#apply-a-claude-code-settings-template) for more policy options.

### 3. Deploy the files and setup tool

Use your MDM to deliver `mdm.toml` and `base-settings.json` to the administrator-controlled directory in the relevant tab. Make both files readable by users and writable only by administrators. Do the same for any parent directories created for the deployment.

Run the corresponding script as root or Administrator/SYSTEM. These scripts expect both input files to have already been delivered; they download the pinned binary only when it is missing or the saved release pin differs.

> [tab] macOS
>
> Deliver both files to `/Library/Application Support/TrueFoundry/`. Save this script in your MDM and run it as root. It supports Apple Silicon and Intel Macs.
>
> ```bash deploy-mdm.sh theme={"dark"}
> #!/bin/bash
> set -euo pipefail
> [[ "$(id -u)" -eq 0 ]] || { echo "Run as root" >&2; exit 1; }
>
> CONFIG="/Library/Application Support/TrueFoundry/mdm.toml"
> BINARY="/usr/local/bin/tfy-local-ai-setup"
> RELEASE="v2.2.0"
> SETTINGS="$(dirname "$CONFIG")/base-settings.json"
> [[ -f "$SETTINGS" ]] || { echo "Deploy $SETTINGS first" >&2; exit 1; }
> [[ -f "$CONFIG" ]] || { echo "Deploy $CONFIG first" >&2; exit 1; }
> chown root:wheel "$(dirname "$CONFIG")" "$CONFIG" "$SETTINGS"
> chmod 755 "$(dirname "$CONFIG")"
> chmod 644 "$CONFIG" "$SETTINGS"
> case "$(uname -m)" in
>   arm64) ARCH="arm64" ;;
>   x86_64) ARCH="amd64" ;;
>   *) echo "Unsupported architecture" >&2; exit 1 ;;
> esac
> mkdir -p "$(dirname "$BINARY")"
> INSTALLED="$(cat "$BINARY.version" 2>/dev/null || true)"
> if [[ ! -x "$BINARY" || "$INSTALLED" != "$RELEASE" ]]; then
>   TMP="$(mktemp "${BINARY}.download.XXXXXX")"
>   trap 'rm -f "$TMP"' EXIT
>   curl -fLsS "https://github.com/truefoundry/tfy-local-ai-setup/releases/download/${RELEASE}/tfy-local-ai-setup-darwin-${ARCH}" -o "$TMP"
>   chown root:wheel "$TMP"
>   chmod 755 "$TMP"
>   mv -f "$TMP" "$BINARY"
>   printf '%s\n' "$RELEASE" > "$BINARY.version"
> fi
> exec "$BINARY" --config="$CONFIG" --settings-file="$SETTINGS" \
>   --log-file="/var/log/tfy-mdm-setup.log"
> ```

> [tab] Linux
>
> Deliver both files to `/etc/truefoundry/`. Run this script as root through your configuration manager or scheduler. The published Linux binary is for x86-64. Set `TFY_MDM_USER` in the scheduled task to the employee account being configured. Linux resolves user context through `SUDO_USER` or `logname`; a root cron/management service generally has neither. Schedule each employee account deliberately rather than guessing a user from a headless service.
>
> ```bash deploy-mdm.sh theme={"dark"}
> #!/bin/bash
> set -euo pipefail
> [[ "$(id -u)" -eq 0 ]] || { echo "Run as root" >&2; exit 1; }
> [[ "$(uname -m)" == "x86_64" ]] || { echo "Requires x86-64" >&2; exit 1; }
>
> TARGET_USER="${TFY_MDM_USER:-${SUDO_USER:-}}"
> [[ -n "$TARGET_USER" ]] || { echo "Set TFY_MDM_USER to the employee account" >&2; exit 1; }
> [[ "$(id -u "$TARGET_USER")" -ne 0 ]] || { echo "Select a non-root employee" >&2; exit 1; }
> export SUDO_USER="$TARGET_USER"
> CONFIG="/etc/truefoundry/mdm.toml"
> BINARY="/usr/local/bin/tfy-local-ai-setup"
> RELEASE="v2.2.0"
> SETTINGS="$(dirname "$CONFIG")/base-settings.json"
> [[ -f "$SETTINGS" ]] || { echo "Deploy $SETTINGS first" >&2; exit 1; }
> [[ -f "$CONFIG" ]] || { echo "Deploy $CONFIG first" >&2; exit 1; }
> chown root:root "$(dirname "$CONFIG")" "$CONFIG" "$SETTINGS"
> chmod 755 "$(dirname "$CONFIG")"
> chmod 644 "$CONFIG" "$SETTINGS"
> mkdir -p "$(dirname "$BINARY")"
> INSTALLED="$(cat "$BINARY.version" 2>/dev/null || true)"
> if [[ ! -x "$BINARY" || "$INSTALLED" != "$RELEASE" ]]; then
>   TMP="$(mktemp "${BINARY}.download.XXXXXX")"
>   trap 'rm -f "$TMP"' EXIT
>   curl -fLsS "https://github.com/truefoundry/tfy-local-ai-setup/releases/download/${RELEASE}/tfy-local-ai-setup-linux-amd64" -o "$TMP"
>   chown root:root "$TMP"
>   chmod 755 "$TMP"
>   mv -f "$TMP" "$BINARY"
>   printf '%s\n' "$RELEASE" > "$BINARY.version"
> fi
> exec "$BINARY" --config="$CONFIG" --settings-file="$SETTINGS" \
>   --log-file="/var/log/tfy-mdm-setup.log"
> ```

> [tab] Windows
>
> Deliver both files to `C:\Program Files\TrueFoundry\`. Run this PowerShell script as Administrator or SYSTEM, using a **64-bit** process. Configure the MDM package for x64 Windows and retain the administrator-only write permissions inherited from Program Files.
>
> ```powershell deploy-mdm.ps1 theme={"dark"}
> $ErrorActionPreference = 'Stop'
> $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
> $principal = [Security.Principal.WindowsPrincipal]::new($identity)
> if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
>   throw 'Run as Administrator or SYSTEM'
> }
> if (-not [Environment]::Is64BitProcess) { throw 'Use 64-bit PowerShell' }
>
> $setupDir = Join-Path $env:ProgramFiles 'TrueFoundry'
> $config = Join-Path $setupDir 'mdm.toml'
> $settings = Join-Path $setupDir 'base-settings.json'
> $binary = Join-Path $setupDir 'tfy-local-ai-setup.exe'
> $release = 'v2.2.0'
> if (-not (Test-Path -LiteralPath $config)) { throw "Deploy $config first" }
> if (-not (Test-Path -LiteralPath $settings)) { throw "Deploy $settings first" }
> $installed = if (Test-Path -LiteralPath "$binary.version") {
>   (Get-Content -LiteralPath "$binary.version" -Raw).Trim()
> } else { '' }
> if (-not (Test-Path -LiteralPath $binary) -or $installed -ne $release) {
>   $download = Join-Path $setupDir ([IO.Path]::GetRandomFileName())
>   try {
>     $uri = "https://github.com/truefoundry/tfy-local-ai-setup/releases/download/$release/tfy-local-ai-setup-windows-amd64.exe"
>     Invoke-WebRequest -Uri $uri -OutFile $download
>     Move-Item -LiteralPath $download -Destination $binary -Force
>     Set-Content -LiteralPath "$binary.version" -Value $release
>   } finally {
>     if (Test-Path -LiteralPath $download) { Remove-Item -LiteralPath $download }
>   }
> }
> & $binary "--config=$config" "--settings-file=$settings" "--log-file=$setupDir\mdm-setup.log"
> exit $LASTEXITCODE
> ```

Keep the binary at that stable, administrator-owned path. Clients and user-context setup may call it later to retrieve credentials. Schedule the wrapper **hourly**, and allow an administrator to trigger it on demand after onboarding or a policy change. The tool installs no scheduler itself.

> [accordion] Preview the configuration before applying it
>
> After installing the binary and delivering the TOML, invoke the binary directly with `--dry-run`:
>
> ```bash theme={"dark"}
> # macOS, as an administrator
> sudo /usr/local/bin/tfy-local-ai-setup \
>   --config="/Library/Application Support/TrueFoundry/mdm.toml" \
>   --settings-file="/Library/Application Support/TrueFoundry/base-settings.json" --dry-run
> # Linux: use /etc/truefoundry/mdm.toml and /etc/truefoundry/base-settings.json.
> ```
>
> ```powershell theme={"dark"}
> # Windows, in an elevated PowerShell terminal
> & 'C:\Program Files\TrueFoundry\tfy-local-ai-setup.exe' `
>   '--config=C:\Program Files\TrueFoundry\mdm.toml' `
>   '--settings-file=C:\Program Files\TrueFoundry\base-settings.json' --dry-run
> ```
>
> Manual mode previews the policy and work available without credentials; it does not simulate a completed employee sign-in. The deployment wrappers above install files and apply configuration. Adding a dry-run flag to the binary invocation does not undo the wrapper's installation steps.

### 4. Ask employees to sign in

After the first privileged run has saved the organization policy, give employees the following instructions. Run in a normal terminal as the employee, **without sudo or elevation**:

> [tab] macOS and Linux
>
> ```bash theme={"dark"}
> /usr/local/bin/tfy-local-ai-setup login --mdm
> ```

> [tab] Windows
>
> ```powershell theme={"dark"}
> & 'C:\Program Files\TrueFoundry\tfy-local-ai-setup.exe' login --mdm
> ```

Complete the browser sign-in. If the tenant requires a team and IT did not set `auth.pat_team`, select an available team when prompted. To specify one explicitly, append `--pat-team=engineering`. The server checks whether the employee can create a PAT for that team.

The command reuses a valid managed PAT on subsequent runs. An expired or revoked PAT requires sign-in to create a replacement. A temporary network or verification error does not create another PAT; retry when connectivity returns.

On completion, the command reports that user setup is complete and administrator-owned updates finish on the next MDM run. Trigger that run for your pilot, then open a new terminal and restart the configured clients. If no user is signed in, user-owned work waits until a user session is available.

### 5. Verify the connection

1. **Check the MDM log.** Confirm the post-login run completed. Exit `0` indicates success or a clean no-op; exit `1` indicates an error. Pending manual onboarding can still produce a successful scheduled run, so check the full onboarding sequence.
2. **Send a model request in each selected client.** Ask: `Reply exactly TFY-CHAT-OK. Do not use tools.` Confirm there is no authentication or model error.
3. **Confirm the route.** Open **AI Gateway → Request Logs** in TrueFoundry and find the request under the expected user and model. A response alone does not prove it went through the gateway.
4. **Check optional MCP servers and Skills.** Authorize MCP servers in each client, then try a harmless read operation. Check the client's Skills list; Desktop Skills require manual upload.

| Client | MCP authorization after deployment |
| - | - |
| Claude Code | Open `/mcp` in the client |
| Codex | Run `codex mcp login <server-name>` |
| OpenCode | Run `opencode mcp auth <server-name>` |
| Gemini CLI | Run `/mcp auth <server-name>` inside the client |
| Claude Desktop | Open **Customize → Connectors → Connect** |

For other clients, follow the app's authorization prompts and the relevant [integration guide](https://www.truefoundry.com/docs/ai-gateway/coding-agents-governance.md). VS Code can require API key entry through its model UI. Clients that use shell environment variables should be launched from a new terminal that has loaded the updated environment.

## Supported clients

**Managed** means setup writes administrator-owned machine configuration. **User** means setup writes the employee's configuration and reconciles it on later runs; users can edit those files. Skills are per-user even for managed clients.

| Client ID in `tools` | Platforms | Configuration channel and limits |
| - | - | - |
| `claude-code` | macOS, Linux, Windows | Managed gateway, tier models, auth, MCP; user Skills |
| `codex` | macOS, Linux, Windows | Managed/default configuration for CLI and desktop app; see Windows version requirement below; user Skills |
| `claude-desktop` | macOS, Windows | Managed preferences/registry, gateway, tier models, auth, MCP; Skills staged as ZIPs for manual upload |
| `opencode` | macOS, Linux, Windows | Managed model/provider configuration and MCP; user Skills |
| `qwen-code` | macOS, Linux, Windows | Managed settings and MCP; user Skills |
| `gemini-cli` | macOS, Linux, Windows | Managed model/MCP/auth type plus user shell environment for gateway URL and credential |
| `grok-build` | macOS, Linux, Windows | Managed requirements and defaults on macOS/Linux; user configuration on Windows. User config can override the managed default model/MCP; requirements enforce model definitions |
| `pi` | macOS, Linux, Windows | User models, MCP, Skills |
| `copilot-cli` | macOS, Linux, Windows | User models, MCP, Skills |
| `goose` | macOS, Linux, Windows | User models, MCP, Skills |
| `vscode` | macOS, Linux, Windows | User configuration; app steps may still be needed for model authentication |
| `cursor` | macOS, Linux, Windows | MCP and Skills only; this flow does not configure gateway inference |

The platform list describes the setup writers. Install a compatible client version separately. Windows examples target native clients; WSL requires separate deployment into the distribution.

> [!NOTE]
>
> **Client compatibility matters.** Claude Code's additive `managedMcpServers` needs Claude Code **2.1.259+**. Claude Desktop managed MCP connectors need Desktop **1.2581.0+**. On Windows, Codex gateway enforcement through `requirements.toml` needs Codex **0.156.0+**; the companion machine `config.toml` supplies defaults on older versions, which users can override.

## TOML configuration reference

Pass this file through `--config=/absolute/path/mdm.toml`. It is separate from personal mode's `~/.tf/config.toml` and the generated `agents-policy.json`. TOML and JSON are supported; YAML is not supported by the full MDM loader. Unknown keys are rejected before authentication or configuration writes.

### Connection and client selection

| Field | Type | Required/default | Purpose and example |
| - | - | - | - |
| `url` | String | Required | HTTPS control-plane origin, such as `https://acme.truefoundry.cloud` |
| `tenant` | String | Required | TrueFoundry tenant name, such as `acme` |
| `gateway` | String | Defaults to `url` | Gateway base URL including its path. Set it explicitly when different from the control plane |
| `tools` | Array of strings | Recommended | Canonical client IDs from the support table. An explicit list selects exactly those clients, even before installation. `[]` disables auto-detection. If omitted, setup auto-detects Claude Code, Codex, and Claude Desktop; additional clients must be selected explicitly |

### Authentication

| Field | Type | Required/default | Behavior |
| - | - | - | - |
| `auth.mode` | String | `"api-key"` | `"api-key"` uses per-user TrueFoundry credentials; `"oidc"` uses OIDC |
| `auth.login` | String | Omitted | Set `"manual"` for deferred PAT onboarding. Omit for the existing automatic flow. Manual onboarding requires compatible `api-key` auth settings; use the all-API-key baseline above |
| `auth.pat_team` | String | Optional | Team owner used when creating the manual-onboarding PAT, such as `"engineering"`. Employee CLI `--pat-team` overrides it |
| `auth.oidc.issuer` | String | Required for OIDC | Exact HTTPS issuer, usually the control-plane origin followed by `/<tenant>` |
| `auth.oidc.client_id` | String | Required for OIDC | Public OIDC client ID, such as `"cli_example"`; no client secret |
| `agents.<id>.auth` | String | Inherits `auth.mode` | `"api-key"` or `"oidc"`. Claude Code, Codex, and Desktop can differ. All selected additional clients must share one auth mode because they share a credential helper |

### Shared models, MCP servers, and Skills

| Field | Type | Required/default | Behavior |
| - | - | - | - |
| `defaults.models` | Array of strings | Needed for selected model-list clients unless overridden | Gateway model/Virtual Model IDs offered by OpenCode, Qwen Code, Grok Build, Pi, Gemini CLI, Copilot CLI, Goose, and VS Code |
| `defaults.default_model` | String | First shared model | Initial model for those clients; must belong to the configured list |
| `mcp.servers` | Array of strings | Optional | Exact gateway registry names, such as `["github", "linear"]`. Each becomes `<gateway>/<tenant>/mcp/<name>/server` |
| `mcp.discovery` | String | Optional | `"all-accessible"` discovers the current user's accessible servers. Cannot be combined with a nonempty shared server list, `--mcp-server`, or `--no-mcp` |
| `skills.names` | Array of strings | Optional | Registry references in `[repo/]name[:version]` form, such as `["engineering/code-review:1"]`. Pin a version for a fixed rollout; omit the version to follow latest |
| `agents.<id>.mcp_servers` | Array of strings | Inherits shared MCP selection | Replaces the shared list or discovery result for that client. `[]` disables MCP deployment for the client |
| `agents.<id>.skills` | Array of strings | Inherits shared Skills | Replaces the shared list. `[]` disables Skills for the client |

For MCP and Skill overrides, **omitted** means inherit and **empty** means disable. Do not assume the same empty-list behavior for model lists; supply a nonempty model list for model-list clients.

### Client model fields

| Field under `[agents.<id>]` | Clients | Default/behavior |
| - | - | - |
| `opus_model`, `sonnet_model`, `haiku_model` | Claude Code, Claude Desktop | Gateway IDs for the three tiers. Claude Code defaults to `claude-code/claude-opus`, `claude-code/claude-sonnet`, and `claude-code/claude-haiku`. Desktop inherits the corresponding CLI tier values unless overridden |
| `model` | Codex | Default gateway model ID. If omitted, the managed writer does not set a default Codex model |
| `models` | Model-list clients listed above | Nonempty per-client list replaces `defaults.models` |
| `default_model` | Model-list clients | Default must be in the effective model list; when omitted, the first model is used |

> [accordion] Additional client options and accepted API values
>
> Only set options supported by the specific client. Unsupported options fail validation.
>
> | Client | Additional supported keys | `api` values, when supported |
> | - | - | - |
> | OpenCode | `headers`, `context_window`, `max_tokens`, `gateway_only` | — |
> | Qwen Code | `headers`, `context_window`, `api` | `"chat-completions"`, `"responses"` |
> | Grok Build | `headers`, `context_window`, `max_tokens`, `api` | `"chat_completions"`, `"responses"`, `"messages"` |
> | Pi | `headers`, `context_window`, `max_tokens`, `api` | `"openai-completions"`, `"openai-responses"`, `"anthropic-messages"` |
> | Gemini CLI | `headers`, `provider_account` | — |
> | Copilot CLI | `headers`, `context_window`, `max_tokens`, `api` | `"completions"`, `"responses"` |
> | Goose | `headers`, `context_window` | — |
> | VS Code | `headers`, `context_window`, `max_tokens`, `api`, `utility_model`, `insiders` | `"chat-completions"`, `"responses"`, `"messages"` |
> | Cursor | Shared/per-client MCP and Skills only | — |
>
> | Key | Type | Meaning |
> | - | - | - |
> | `headers` | Array of strings | Extra inference headers in `"Name: Value"` form, such as `["X-Department: engineering"]`. Omit to add none. Use `--desktop-header` for Desktop; its TOML `headers` option fails validation in v2.2.0 |
> | `context_window` | Integer | Declared context size in tokens; defaults to `128000` where emitted. Use a positive value matching the model |
> | `max_tokens` | Integer | Declared maximum output tokens; defaults to `16384` where emitted. Use a positive value matching the model |
> | `api` | String | Wire protocol used by the client. Defaults: Qwen Code/VS Code `chat-completions`, Grok Build `chat_completions`, Pi `openai-completions`, Copilot CLI `completions`. Select a protocol supported by your model |
> | `gateway_only` | Boolean | OpenCode: `true` hides other providers in the generated configuration; omitted/`false` leaves them available |
> | `provider_account` | String | Gemini provider account name for native routing through `<gateway>/gemini/<account>/proxy`; set it for native Gemini routing |
> | `utility_model` | String | VS Code background tasks: `"mainAgent"` uses the main gateway model, `"copilot"` uses Copilot, `"none"` disables it. Defaults to `"mainAgent"` |
> | `insiders` | Boolean | VS Code: `true` targets Insiders; omitted/`false` targets stable |

### Precedence and unsupported fields

Explicit command-line flags override corresponding TOML values. For example, `--codex-default-model=gpt-5.4` changes Codex's model for that run. Keep scheduled overrides in your deployment package so the next run does not revert them.

* `--mcp-server` replaces only the shared MCP list; per-client lists remain. `--no-mcp` suppresses the shared list and preserves per-client overrides.
* `--no-skills` removes MDM-installed Skills, including per-client selections.
* `--agent` replaces the additional-client selection from the file; Claude Code, Codex, and Desktop retain their separate selection flags. Use `--claude-desktop=false`, for example, to deselect Desktop.
* `--agents-config` is the older partial input mode. It cannot be combined with `--config`.
* `[[mcp.custom]]` is not propagated through the MDM policy in v2.2.0. Register the server in the gateway and use its registry name instead.
* `[restrictions]` is not accepted by the full MDM `--config` parser in v2.2.0. Use the [prompt-blocking CLI flags](#block-new-prompts-in-a-client).

## Practical examples

The following snippets **replace or extend the matching sections** of the baseline TOML. Do not append a second copy of an existing table.

### Give clients different MCP servers and Skills

```toml theme={"dark"}
[mcp]
servers = ["github", "linear"]

[skills]
names = ["engineering/code-review:1"]

[agents.claude-code]
opus_model = "claude-code/claude-opus"
sonnet_model = "claude-code/claude-sonnet"
haiku_model = "claude-code/claude-haiku"
mcp_servers = ["github"]

[agents.codex]
model = "gpt-5.4"
skills = []
```

Claude Code receives GitHub and the pinned code-review Skill. Codex receives GitHub and Linear, but no Skills. OpenCode inherits both shared lists. Replace the names with accessible entries in your registries. These settings configure MCP connections; employees still authorize them in each client.

### Discover each employee's accessible MCP servers

Replace the baseline `[mcp]` table with:

```toml theme={"dark"}
[mcp]
discovery = "all-accessible"
```

After sign-in, setup lists registry servers and checks `USE_MCP_SERVER` permission to build each user's list. Discovery refreshes on scheduled runs without opening a browser; it does not initiate MCP OAuth consent. A per-client `mcp_servers` list still overrides discovery.

Failed or incomplete discovery retains existing MCP configuration. A successful discovery with no accessible servers removes previously discovered entries. The gateway continues checking authorization on requests independently of client refresh. Restart clients that need it to pick up changes.

### Add another model-list client

Add `qwen-code` to the top-level `tools` list, then add:

```toml theme={"dark"}
[agents.qwen-code]
models = ["gpt-5.4", "reasoning-model"]
default_model = "reasoning-model"
api = "responses"
mcp_servers = ["github"]
```

Qwen Code uses its own model list and default, while OpenCode keeps `[defaults]`. Both inherit the baseline API-key/manual login method. Choose gateway model IDs and a wire protocol supported by your actual models.

## Other authentication methods

| Method | Configuration | When authentication happens |
| - | - | - |
| Manual PAT onboarding | `auth.mode = "api-key"`, `auth.login = "manual"` | Employee runs `login --mdm`; scheduled runs reuse the PAT |
| OIDC | `auth.mode = "oidc"`, issuer/client ID; omit `auth.login` | Claude Code/Codex use a runtime helper; Desktop signs in natively. Additional clients use the shared helper flow |
| Automatic device login | `auth.mode = "api-key"`; omit `auth.login` | Setup uses the employee's device-login refresh token, opening a browser when initial sign-in or reauthentication is needed |

### OIDC

Create a **public native OIDC client** on your TrueFoundry control plane. With a PAT whose owner can manage tenant settings, send:

```bash theme={"dark"}
curl --request PUT 'https://acme.truefoundry.cloud/api/svc/v1/oidc/clients' \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer $TFY_ADMIN_PAT" \
  --data '{
    "manifest": {
      "name": "ai-clients-mdm",
      "display_name": "AI clients MDM",
      "client_type": "public",
      "redirect_uris": ["http://127.0.0.1/callback", "http://127.0.0.1:8585/callback", "http://127.0.0.1:8586/callback", "http://127.0.0.1:8587/callback"],
      "allowed_scopes": ["openid", "profile", "email", "offline_access"],
      "allowed_grant_types": ["authorization_code", "refresh_token"],
      "token_endpoint_auth_method": "none",
      "pkce": "required",
      "application_type": "native",
      "access_token_ttl": "1d",
      "refresh_token_ttl": "7d",
      "refresh_token_max_uses": 1
    }
  }'
```

Replace the origin for a self-hosted control plane. Copy `data.id` from the response as the client ID. Replace the baseline authentication table with:

```toml theme={"dark"}
[auth]
mode = "oidc"

[auth.oidc]
issuer = "https://acme.truefoundry.cloud/acme"
client_id = "cli_example"
```

Use the exact `issuer` from `<issuer>/.well-known/openid-configuration`. Omit `login = "manual"` and any API-key overrides when using this all-OIDC example. Do not instruct these employees to run `login --mdm`; that command is for manual PAT policy.

Claude Code receives `apiKeyHelper`; Codex receives an auth command pointing to the installed binary. Their first runtime sign-in opens a browser; subsequent requests reuse cached tokens or refresh them. Desktop uses its own native session. v2.2.0 defaults Desktop to `access_token` with `openid profile email offline_access` scopes; TrueFoundry rejects an ID token as the gateway bearer token.

MCP discovery and authenticated Skill downloads still require usable credentials. Complete sign-in before expecting those operations to succeed on scheduled runs. Primary clients can override auth independently; all selected additional clients must agree on a single auth mode.

### Automatic device login

Keep `mode = "api-key"` and remove `login = "manual"`. The privileged run authenticates in the logged-in employee's context, first trying their saved device-login refresh token. It can open a browser if sign-in is needed. Hourly runs keep the generated credentials current.

This flow uses `~/.tf/refresh-token` and `~/.tf/access-token`, separately from the manual PAT store and OIDC's `~/.tf/oidc-*` caches. The `login --mdm` command applies only to manual PAT policy.

## Advanced client controls

These options are **CLI flags**, not additional TOML keys. Add them to the wrapper's binary invocation as needed.

### Apply a Claude Code settings template

Use `--settings-file=/absolute/path/base-settings.json` to select your organization's permission and sandbox policy. The binary merges gateway, model, auth, and MCP settings into that template. With no explicit template it reuses an existing managed settings file, otherwise its built-in baseline applies.

Review the baseline before rollout: it includes denied shell/network and sensitive-file operations, approval rules, managed-only permission/hook rules, 14-day transcript retention, and a sandbox configuration with proxy ports **8080/8081**. Supply your own template when those proxy services or restrictions do not match your environment.

The [starter template](#2-prepare-the-claude-code-policy-template) above provides a complete example without a sandbox proxy.

For Claude Code, gateway MCP entries are additive. To restrict other MCP servers, configure `allowedMcpServers`/`deniedMcpServers` in your template. Deploy the template under administrator ownership, alongside the TOML. Review any Anthropic server-managed policies with your Claude administrator because they can change the effective policy.

### Block new prompts in a client

Run as root/Administrator, independently or alongside setup:

```bash theme={"dark"}
sudo /usr/local/bin/tfy-local-ai-setup \
  --block-agent=claude-code,codex,cursor \
  --block-message="Use your organization's approved AI client. Contact IT for access."

# Remove only this tool's Cursor restriction.
sudo /usr/local/bin/tfy-local-ai-setup --unblock-agent=cursor
```

On Windows, use the installed `.exe` from an elevated PowerShell terminal and omit `sudo`. Hooks block prompt submission, not launching apps, manual editing, Cursor Tab completion, or other clients. Claude Code's fail-closed hook requires **2.1.295+**; Codex hooks can require trust review through `/hooks`. Restart the client or start a new session after a change. Native Windows hooks do not cover WSL clients.

### Other CLI options

| Flags | Purpose |
| - | - |
| `--claude-gateway`, `--codex-gateway`, `--claude-desktop-gateway` | Override the shared gateway for a primary client |
| `--codex-settings-file` | Base Codex TOML template; the provider section is injected |
| `--codex-auth-mode=chatgpt-subscription` | Use ChatGPT subscription auth with a gateway `x-tfy-api-key` header. Requires Codex API-key auth; see [subscription setup](https://www.truefoundry.com/docs/ai-gateway/mcp/enterprise-security-codex.md#using-codex-mdm-with-a-chatgpt-business-or-enterprise-subscription) |
| `--desktop-header='Name: Value'` | Add a Desktop inference header; repeatable |
| `--force-login-org=UUID` | Restrict Desktop sign-in to specified Claude organizations; repeatable |
| `--claude-desktop-allow-local-mcp=false` | Disable Desktop local stdio MCP servers |
| `--disable-deployment-mode-chooser` | Hide Desktop's Claude.ai profile escape; applies to Desktop OIDC |
| `--oidc-scopes`, `--oidc-bearer-token-type` | Desktop OIDC scopes and bearer choice. Keep `openid`; use `access_token` for TrueFoundry |
| `--claude-ai-import` | Enable Desktop claude.ai history import; requires Desktop 1.10628.0+. Team/Enterprise export permissions still apply |
| `--import-export-enabled=false`, `--import-banner-behavior=off`, `--import-automatic-3p`, `--import-encoding=native-dict` | Customize enabled Desktop import. Banner values: `off`, `detect`, `show`; macOS encoding: `json-string` (default) or `native-dict` |
| `--wsl` | Windows: also push supported Claude Code/Codex managed configuration into the logged-in user's WSL distributions |
| `--write-config-on-login-failure`, `--placeholder-token` | Automatic device-login flow: write cached/placeholder credentials on login failure and report failure. This is not a replacement for manual onboarding |
| `--refresh-token-file`, `--access-token-file` | Override token-cache paths for flows that use those caches |
| `--log-file`, `--debug` | Append logs to a protected file and enable verbose diagnostics |

Desktop tool search and 1M-context tuning require wrapper-level customizations; they are not full-TOML fields. The starter wrappers above leave these client defaults unchanged. If you add managed plist keys outside the binary, apply them **after every binary run**: the binary rebuilds and locks the Desktop plist. Claude Code's `[1m]` model suffix does not substitute for Desktop's model capability entries. Test tuning with your Desktop version before rollout.

## Operate the rollout

Update the administrator-controlled TOML through MDM, then trigger the wrapper or wait for its hourly schedule. Keep changes to the release pin, TOML, settings templates, and CLI flags in the same deployment package. Restart clients that read configuration only at launch.

### Files and credentials

| Item | macOS | Linux | Windows |
| - | - | - | - |
| Organization policy | `/Library/Application Support/TrueFoundry/agents-policy.json` | `/etc/truefoundry/agents-policy.json` | `%ProgramFiles%\TrueFoundry\agents-policy.json` |
| Claude Code managed settings | `/Library/Application Support/ClaudeCode/managed-settings.json` | `/etc/claude-code/managed-settings.json` | `%ProgramFiles%\ClaudeCode\managed-settings.json` |
| Codex managed configuration | `/etc/codex/managed_config.toml` | `/etc/codex/managed_config.toml` | `%ProgramData%\OpenAI\Codex\requirements.toml` and `config.toml` |
| Claude Desktop policy | `/Library/Managed Preferences/<user>/com.anthropic.claudefordesktop.plist` | Not supported | `HKLM\SOFTWARE\Policies\Claude` |

The organization policy contains configuration, never PAT values. Manual PAT credentials are stored in the employee's OS credential store, scoped to control-plane URL and tenant separately from personal mode. When that store is unavailable, the owner-only fallback is `~/.tf/personal-mdm-<account-hash>-pat`.

API-key mode can write credentials into generated client configuration. Protect those files and avoid sharing their contents. User reconciliation state, including pending work, is in `~/.tf/mdm-user.json`. Skill bundles are cached under `~/.tf/cache/skills/<tenant>/<version-id>` and installed into each client's Skills directory. Desktop ZIPs are staged under `~/.tf/skills/claude-desktop`.

For a safe policy inspection, check selected IDs without printing generated credentials:

```bash theme={"dark"}
# macOS
/usr/bin/plutil -extract agents json -o - \
  "/Library/Application Support/TrueFoundry/agents-policy.json"
# Linux (requires jq)
jq '.agents | keys' /etc/truefoundry/agents-policy.json
```

```powershell theme={"dark"}
$policy = Get-Content "$env:ProgramFiles\TrueFoundry\agents-policy.json" -Raw | ConvertFrom-Json
$policy.agents.PSObject.Properties.Name
```

### Failure and removal behavior

* **Employee has not signed in:** manual mode saves intent and leaves credential-dependent work pending. Ask the employee to run `login --mdm`, then trigger the privileged run.
* **Credential verification or discovery fails:** existing credential-dependent/MCP settings are retained for retry. Read logs for pending work; a successful initial scheduled run is not proof of completed onboarding.
* **Discovery succeeds with an empty result:** previously discovered MCP entries are removed. This differs from a failed discovery.
* **MCP or Skill selection changes:** later reconciliation removes entries installed by this tool that are no longer selected. Pinned Skills stay at the chosen version; unpinned references can update on later runs. Cached bundles are reused, although registry metadata checks can still occur.
* **A client is removed from `tools`:** tracked additional-client settings are cleaned up through reconciliation. Do not assume this removes the primary clients' legacy managed files. To decommission Claude Code, Codex, or Desktop, stop scheduled reapplication and explicitly remove the protected files/registry policy through your MDM's administrator workflow.

There is no single fleet-wide uninstall command in this guide. Personal-mode `uninstall` is not an MDM decommissioning workflow. Remove configured prompt restrictions with `--unblock-agent` before retiring the binary. Revoke PATs in TrueFoundry when access is no longer needed.

## FAQ

> [accordion] Why does login --mdm say there is no organization policy?
>
> The privileged setup has not saved a manual-onboarding policy yet. Deliver the TOML and run the MDM wrapper first. Also confirm the employee is using the administrator-installed binary rather than an older copy on PATH.

> [accordion] Why is the client still unauthenticated after the employee signs in?
>
> User sign-in cannot modify administrator-owned settings. Trigger the next privileged MDM run, check its log, and restart the client. Also verify the employee can access the selected gateway model. VS Code may require an additional API key action in its model UI.

> [accordion] Does the scheduled run open a browser or renew the one-year PAT?
>
> With `auth.login = "manual"`, scheduled runs do not open a browser and do not mint replacement PATs. They reuse the saved PAT. When it expires or is revoked, the employee runs `login --mdm` again. Automatic device login and OIDC have different refresh behavior.

> [accordion] Can employees override settings or use other providers?
>
> It depends on the client's channel and version. Protected managed settings enforce the controls the client supports. User-channel settings can be edited and are reapplied later. MCP deployment is not automatically an allowlist, and configuring a gateway does not itself block network access to other providers. Use the client's policy controls and your organization's network controls where needed.

> [accordion] Does discovering or deploying an MCP server sign the employee into it?
>
> No. Discovery selects accessible servers and deployment writes connection details. Employees still complete each client's MCP authorization flow. Gateway permissions continue to apply independently.

> [accordion] Can I deploy a custom MCP URL or a local stdio server in this TOML?
>
> The v2.2.0 MDM policy propagates gateway registry names. It does not propagate `mcp.custom`. Register supported remote servers in the gateway and reference their names. Local server controls are client-specific; Desktop local MCP can be disabled with its CLI flag.

> [accordion] Why did a shared list not affect one client?
>
> A per-client `mcp_servers` or `skills` list replaces the shared list. An explicit empty list disables that selection. Remove the per-client field to inherit again, and check whether your wrapper passes overriding CLI flags.

> [accordion] Why do Desktop Skills need an upload?
>
> Setup prepares upload-ready ZIP files under `~/.tf/skills/claude-desktop`. Upload them through Desktop's Skills UI. Other supported clients receive Skills in their own directories; deploying a new ZIP does not replace a previously uploaded Desktop Skill inside the app.

> [accordion] What happens when nobody is logged in?
>
> User-context work waits for an employee session. Credential-dependent managed writes may also remain pending. Run the pilot with an employee signed in, then verify the next scheduled run completes the remaining work. Do not treat device-level script success as proof that every user's profile has been configured.

> [accordion] Can employees also use personal setup?
>
> Personal setup recognizes clients listed in the organization policy and leaves their management to IT. Managed PAT credentials and applied user state are separate from personal-mode state. Use personal setup for clients outside the managed policy and review any client-native policy precedence.

> [accordion] What should I collect before contacting support?
>
> Collect the setup release, OS and client versions, sanitized TOML, relevant MDM log errors, and the stage that failed: deployment, employee login, privileged refresh, model request, MCP authorization, or Skill installation. Do not include PATs, token-cache contents, or generated client files containing credentials.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.