> ## 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.

# Securing Claude Code with TrueFoundry

> Govern Claude Code in the enterprise: route model and MCP traffic through TrueFoundry and enforce settings across developer machines with MDM.

This guide covers how to secure **Claude Code (CLI)** in the enterprise using TrueFoundry. The focus is on the controls you own and configure:

* **Route all model traffic** through the [TrueFoundry AI Gateway](/docs/ai-gateway/intro-to-llm-gateway) for centralized access control, routing, and cost governance.
* **Govern MCP access** through the [TrueFoundry MCP Gateway](/docs/ai-gateway/mcp/mcp-overview) so Claude only reaches trusted, approved tools.
* **Enforce settings** consistently across every developer machine using MDM.

<Note>
  Identity setup (SSO, domain capture) and admin-console settings are configured in Anthropic's own console and documented in [Claude's docs](https://support.claude.com/en/articles/13132885-set-up-single-sign-on-sso). This guide does not repeat that — it focuses on what's specific to a TrueFoundry deployment.
</Note>

<Info>
  **MDM isn't limited to the CLI.** The same [`tfy-local-ai-setup`](https://github.com/truefoundry/tfy-local-ai-setup) binary this guide deploys also enforces **[Claude Desktop (Cowork 3P)](/docs/ai-gateway/claude-desktop)** fleet-wide by writing and locking the `com.anthropic.claudefordesktop` managed preferences (macOS · Windows) — pass `--claude-desktop` alongside (or instead of) `--claude-code`, or omit both to auto-detect installed tools. Prebuilt binaries for every platform are on the [releases page](https://github.com/truefoundry/tfy-local-ai-setup/releases). The one Claude surface with no endpoint setting is **claude.ai (web)**; govern it with the [aitori](https://github.com/truefoundry/aitori) on-device agent — see [Govern all AI traffic](/docs/ai-gateway/govern-traffic-through-ai-gateway#method-b-aitori-the-open-source-on-device-agent).
</Info>

***

## 1. Identity (prerequisite)

Before rolling out Claude Code, enforce SSO and **domain capture** in the Claude Admin Console so access is tied to your IdP (Okta, Entra ID, Auth0, Google Workspace) and employees can't fall back to personal accounts. See [Claude's SSO setup guide](https://support.claude.com/en/articles/13132885-set-up-single-sign-on-sso).

With TrueFoundry, developers don't manage personal API keys. Authentication is handled automatically by the `tfy-local-ai-setup` binary deployed via MDM (see [Enforce settings with MDM](#4-enforce-settings-with-mdm)) — it fetches a fresh gateway token scoped to the user's TrueFoundry identity and writes it into Claude Code's managed settings.

***

## 2. Route model traffic through TrueFoundry AI Gateway

Point Claude Code at the [TrueFoundry AI Gateway](/docs/ai-gateway/intro-to-llm-gateway) so every request passes through a single control point before reaching any provider:

```bash theme={"dark"}
export ANTHROPIC_BASE_URL=https://<your-truefoundry-gateway-url>
```

Once routed through the Gateway you can:

* **Reach any provider** — add [provider accounts](/docs/ai-gateway/quick-start#add-provider-account-and-models) (Anthropic, Bedrock, Vertex, and more) and access them all through one endpoint.
* **Route with [Virtual Models](/docs/ai-gateway/virtual-model)** — a single model identifier with weight-, latency-, or priority-based routing and failover; switch providers without changing any client config.
* **Enforce controls** — [access control](/docs/ai-gateway/gateway-access-control), [rate limits](/docs/ai-gateway/ratelimiting), and [budget limits](/docs/ai-gateway/budgetlimiting) apply to every request before it reaches the provider.

Map the gateway models to Claude Code's defaults:

```bash theme={"dark"}
export ANTHROPIC_DEFAULT_OPUS_MODEL=claude-code/claude-opus
export ANTHROPIC_DEFAULT_SONNET_MODEL=claude-code/claude-sonnet
export ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-code/claude-haiku
```

<Note>
  Setting environment variables on a single machine is easy to undo. To make routing tamper-resistant across your whole fleet, deploy these via MDM — see [Enforce settings with MDM](#4-enforce-settings-with-mdm).
</Note>

***

## 3. Govern MCP access through TrueFoundry MCP Gateway

MCP servers connect Claude to databases, APIs, and SaaS tools. Each one a developer adds expands the attack surface — unvetted servers bring prompt-injection risk, credential sprawl, and no audit trail of which tools were called or what data was returned.

The recommended posture is to **route all MCP access through the [TrueFoundry MCP Gateway](/docs/ai-gateway/mcp/mcp-overview)** and **allowlist only the gateway URL** in Claude's managed settings. This gives you one control point regardless of how many MCP servers you run:

* **Centralized registry** — register and manage all approved MCP servers in one place.
* **Unified authentication** — developers authenticate once; the Gateway handles outbound auth (API key, OAuth2, token passthrough) to each downstream server. [Learn more](/docs/ai-gateway/mcp/mcp-gateway-auth-security)
* **Role-based access control** — control which users and teams can use which servers and tools.
* **Tool-level governance** — disable individual tools, or expose an approved subset per team via a [virtual MCP server](/docs/ai-gateway/mcp/virtual-mcp-server).
* **Guardrails** — pre/post-execution checks and approval workflows on tool calls. [Learn more](/docs/ai-gateway/guardrails-overview)
* **Full audit trail** — every tool call is traced with user attribution and payloads, exportable via OpenTelemetry. [Learn more](/docs/ai-gateway/analytics-mcp-metrics)

### Lock Claude to the gateway

Allowlist only the gateway URL and block marketplace-sourced installs in `managed-settings.json`:

```json theme={"dark"}
{
  "allowedMcpServers": [
    { "serverUrl": "https://<your-truefoundry-gateway-url>/*" }
  ],
  "strictKnownMarketplaces": []
}
```

### Connect Claude Code to gateway-backed servers

<Steps>
  <Step title="Admin registers MCP servers in TrueFoundry">
    Register approved servers in the TrueFoundry Control Plane, configuring outbound auth, access policies, and guardrails for each.
  </Step>

  <Step title="Developer copies the connection URL">
    Developers log into the TrueFoundry UI to see available servers and copy each ready-to-use connection URL.
  </Step>

  <Step title="Add servers to Claude's config">
    Point each server at the gateway URL:

    ```json theme={"dark"}
    {
      "mcpServers": {
        "truefoundry-github": {
          "type": "url",
          "url": "https://<your-truefoundry-gateway-url>/mcp/v1/github/mcp"
        },
        "truefoundry-sentry": {
          "type": "url",
          "url": "https://<your-truefoundry-gateway-url>/mcp/v1/sentry/mcp"
        }
      }
    }
    ```
  </Step>
</Steps>

On managed devices, pre-seed servers by deploying a `managed-mcp.json` file via MDM. When present, it takes **exclusive control** — developers cannot add MCP servers beyond what's defined:

```json theme={"dark"}
{
  "mcpServers": {
    "truefoundry-github": {
      "type": "http",
      "url": "https://<your-truefoundry-gateway-url>/mcp/v1/github/mcp"
    },
    "truefoundry-sentry": {
      "type": "http",
      "url": "https://<your-truefoundry-gateway-url>/mcp/v1/sentry/mcp"
    }
  }
}
```

**System paths:** macOS `/Library/Application Support/ClaudeCode/managed-mcp.json` · Linux `/etc/claude-code/managed-mcp.json`. Access decisions happen at the gateway, so this file only changes when you add or remove an entire integration.

***

## 4. Enforce settings with MDM

Claude Code reads settings in priority order, with the system-level `managed-settings.json` taking precedence and being un-overridable by developers:

```text theme={"dark"}
System (managed-settings.json)  ← admin-controlled, highest priority
  └── Project (.claude/settings.json)
        └── User (~/.claude/settings.json)  ← developer-controlled
```

**System paths:** macOS `/Library/Application Support/ClaudeCode/managed-settings.json` · Linux `/etc/claude-code/managed-settings.json`.

This single file enforces everything covered above — gateway routing, MCP allowlisting, tool permissions, and sandboxing. A recommended baseline:

```json expandable theme={"dark"}
{
  "permissions": {
    "disableBypassPermissionsMode": "disable",
    "deny": [],
    "ask": [
      "Bash(git push:*)",
      "Write(**)",
      "Bash(curl:*)",
      "Bash(wget:*)",
      "Read(**/.env)",
      "Read(**/.env.*)",
      "Read(**/secrets/**)",
      "Read(**/.ssh/**)",
      "Read(**/credentials/**)"
    ]
  },
  "allowManagedPermissionRulesOnly": true,
  "allowManagedHooksOnly": true,
  "transcriptRetentionDays": 14,
  "sandbox": {
    "enabled": true,
    "network": { "httpProxyPort": 8080, "socksProxyPort": 8081 }
  },
  "allowedMcpServers": [
    { "serverUrl": "https://<your-truefoundry-gateway-url>/*" }
  ],
  "strictKnownMarketplaces": [],
  "env": {
    "ANTHROPIC_BASE_URL": "https://<your-truefoundry-gateway-url>",
    "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-code/claude-opus",
    "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-code/claude-sonnet",
    "ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-code/claude-haiku"
  }
}
```

Key controls (see [Claude's settings reference](https://code.claude.com/docs/en/settings#settings-files) for the full list):

| Setting                           | Effect                                                                                         |
| --------------------------------- | ---------------------------------------------------------------------------------------------- |
| `disableBypassPermissionsMode`    | Blocks `--dangerously-skip-permissions`                                                        |
| `allowManagedPermissionRulesOnly` | Only system rules apply — project/user can't add permissions                                   |
| `allowManagedHooksOnly`           | Prevents rogue hook injection                                                                  |
| `deny` / `ask` rules              | Block outright, or require explicit approval                                                   |
| `sandbox`                         | OS-level filesystem and network isolation ([docs](https://code.claude.com/docs/en/sandboxing)) |

### Deployment scripts

The scripts below deploy and **lock** `managed-settings.json`. On every run the script fetches a fresh gateway token for the logged-in user and writes it into `ANTHROPIC_CUSTOM_HEADERS` — no PAT or manual credentials needed. **Schedule the script to run hourly** so the token stays fresh; the browser device flow only appears on first run or after the refresh token expires.

The scripts download the `tfy-local-ai-setup` binary from the [truefoundry/tfy-local-ai-setup](https://github.com/truefoundry/tfy-local-ai-setup) GitHub repo. The README there documents all available flags, the default `managed-settings.json` config, and advanced usage (custom model IDs, settings templates, manual runs).

<Warning>
  Update the placeholders in the **Config** section before running: `<your-gateway-url>`, `<your-control-plane-url>`, and `<your-tenant-name>`. The script will misconfigure Claude Code if these are left as placeholders.
</Warning>

**Prerequisites**

Before running the MDM script, verify one thing: **no server-managed settings active**. Check the Anthropic Admin Console under **Settings → Policies**. If any server-side policies are enabled, they will conflict with or override the file-based `managed-settings.json`. Disable server-managed settings before proceeding.

**How the script works**

The script runs as root on a schedule (recommended: hourly) and does four things on every execution:

1. **Saves and loads config** — On first run, the three required values (`GATEWAY_URL`, `CONTROL_PLANE_URL`, `TENANT_NAME`) are read from the script's Config section and written to a root-owned config file (macOS: `/Library/Preferences/com.truefoundry.tfy-local-ai-setup.conf`; Linux: `/etc/tfy/tfy-local-ai-setup.conf`; Windows: `C:\ProgramData\TrueFoundry\tfy-local-ai-setup.conf`). On subsequent runs the file is loaded automatically — no changes to the script are needed. A non-empty value in the Config section overrides the saved file (useful for one-off updates).

2. **Installs or updates `tfy-local-ai-setup`** — Downloads the binary from the [`tfy-local-ai-setup` GitHub releases](https://github.com/truefoundry/tfy-local-ai-setup/releases) if it is not present or if the installed version does not match `RELEASE_TAG`. On subsequent runs where the version already matches, this step is skipped entirely.

3. **Detects the logged-in user and fetches a fresh auth token** — The binary identifies who is currently logged into the machine, then silently refreshes the token from `~/.tf/refresh-token`. The browser device-authorization flow only appears the very first time, or after the refresh token has expired. Every other run completes silently in the background.

4. **Writes and locks `managed-settings.json`** — The binary builds the JSON with the gateway URL, model IDs, and the freshly fetched token, then writes it to the system-level path and locks the file so developers cannot modify it (macOS: `chflags schg`; Linux: `chattr +i`; Windows: `icacls` ACL).

<Tabs>
  <Tab title="macOS">
    Run via Jamf, Mosyle, Kandji, or any MDM supporting script execution. Must run as root. Detects Apple Silicon vs Intel automatically.

    ```bash theme={"dark"}
    #!/bin/bash
    # MDM Deployment Script: Claude Code — macOS (arm64 + amd64)
    # Must be run as root.

    set -euo pipefail

    [[ "$(id -u)" -ne 0 ]] && { echo "ERROR: Must be run as root." >&2; exit 1; }

    CONFIG_FILE="/Library/Preferences/com.truefoundry.tfy-local-ai-setup.conf"

    log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }

    # ---------------------------------------------------------------------------
    # Config — fill in on first run. Values are saved to ${CONFIG_FILE} and
    # reloaded automatically on subsequent runs — no edits needed after that.
    # To update a value later, set it here and run the script once more.
    # ---------------------------------------------------------------------------
    GATEWAY_URL="<your-gateway-url>"
    CONTROL_PLANE_URL="<your-control-plane-url>"
    TENANT_NAME="<your-tenant-name>"

    # Optional: per-tool gateway URL overrides (both default to GATEWAY_URL if not set)
    # CLAUDE_GATEWAY_URL=""
    # CODEX_GATEWAY_URL=""

    # Optional: override model IDs (defaults shown — change if using Claude Enterprise or a virtual model)
    # OPUS_MODEL="claude-code/claude-opus"
    # SONNET_MODEL="claude-code/claude-sonnet"
    # HAIKU_MODEL="claude-code/claude-haiku"

    # Optional: path to a JSON template for Claude Code managed-settings.json
    # SETTINGS_FILE="/etc/tfy/base-settings.json"

    # Optional: path to a TOML template for Codex config.toml
    # CODEX_SETTINGS_FILE="/etc/tfy/base-codex-config.toml"

    # ---------------------------------------------------------------------------
    # Load + save config (before binary install)
    # ---------------------------------------------------------------------------
    # Fill in any placeholder values from the saved config file
    if [[ -f "${CONFIG_FILE}" ]]; then
      while IFS='=' read -r _key _value; do
        [[ -z "${_key}" || "${_key}" == \#* ]] && continue
        _key="${_key// /}"; _value="${_value%\"}"; _value="${_value#\"}"
        case "${_key}" in
          GATEWAY_URL)       [[ "${GATEWAY_URL}"       == "<your-gateway-url>" ]]       && GATEWAY_URL="${_value}" ;;
          CONTROL_PLANE_URL) [[ "${CONTROL_PLANE_URL}" == "<your-control-plane-url>" ]] && CONTROL_PLANE_URL="${_value}" ;;
          TENANT_NAME)       [[ "${TENANT_NAME}"       == "<your-tenant-name>" ]]       && TENANT_NAME="${_value}" ;;
        esac
      done < "${CONFIG_FILE}"
    fi

    [[ "${GATEWAY_URL}"       == "<your-gateway-url>" ]]       && { log "ERROR: GATEWAY_URL not set — fill it in above or check ${CONFIG_FILE}."; exit 1; }
    [[ "${CONTROL_PLANE_URL}" == "<your-control-plane-url>" ]] && { log "ERROR: CONTROL_PLANE_URL not set — fill it in above or check ${CONFIG_FILE}."; exit 1; }
    [[ "${TENANT_NAME}"       == "<your-tenant-name>" ]]       && { log "ERROR: TENANT_NAME not set — fill it in above or check ${CONFIG_FILE}."; exit 1; }

    # Persist config for future runs (root-owned, 644 — developers cannot edit)
    cat > "${CONFIG_FILE}" <<CONF
    GATEWAY_URL="${GATEWAY_URL}"
    CONTROL_PLANE_URL="${CONTROL_PLANE_URL}"
    TENANT_NAME="${TENANT_NAME}"
    CONF
    chown root:wheel "${CONFIG_FILE}" && chmod 644 "${CONFIG_FILE}"
    log "Config saved to ${CONFIG_FILE}."

    BINARY_PATH="/usr/local/bin/tfy-local-ai-setup"
    RELEASE_TAG="v1.3.0"
    RELEASE_BASE="https://github.com/truefoundry/tfy-local-ai-setup/releases/download/${RELEASE_TAG}"
    VERSION_FILE="${BINARY_PATH}.version"

    # ---------------------------------------------------------------------------
    # Install binary (skip if already on the correct release tag)
    # ---------------------------------------------------------------------------
    INSTALLED_TAG="$([[ -f "${VERSION_FILE}" ]] && cat "${VERSION_FILE}" || echo '')"

    if [[ ! -f "${BINARY_PATH}" ]] || [[ "${INSTALLED_TAG}" != "${RELEASE_TAG}" ]]; then
      case "$(uname -m)" in
        arm64)  BINARY_URL="${RELEASE_BASE}/tfy-local-ai-setup-darwin-arm64" ;;
        x86_64) BINARY_URL="${RELEASE_BASE}/tfy-local-ai-setup-darwin-amd64" ;;
        *) echo "ERROR: Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
      esac
      log "Installing tfy-local-ai-setup ${RELEASE_TAG} ($(uname -m))..."
      curl -fsSL "${BINARY_URL}" -o "${BINARY_PATH}" && chmod +x "${BINARY_PATH}"
      echo "${RELEASE_TAG}" > "${VERSION_FILE}"
      log "tfy-local-ai-setup installed at ${BINARY_PATH}."
    else
      log "tfy-local-ai-setup ${RELEASE_TAG} already installed — skipping download."
    fi

    # ---------------------------------------------------------------------------
    # Run
    # ---------------------------------------------------------------------------
    exec "${BINARY_PATH}" \
      --claude-code \
      --url="${CONTROL_PLANE_URL}" \
      --tenant="${TENANT_NAME}" \
      --gateway="${GATEWAY_URL}" \
      ${CLAUDE_GATEWAY_URL:+--claude-gateway="${CLAUDE_GATEWAY_URL}"} \
      ${CODEX_GATEWAY_URL:+--codex-gateway="${CODEX_GATEWAY_URL}"} \
      ${OPUS_MODEL:+--opus-model="${OPUS_MODEL}"} \
      ${SONNET_MODEL:+--sonnet-model="${SONNET_MODEL}"} \
      ${HAIKU_MODEL:+--haiku-model="${HAIKU_MODEL}"} \
      ${SETTINGS_FILE:+--settings-file="${SETTINGS_FILE}"} \
      ${CODEX_SETTINGS_FILE:+--codex-settings-file="${CODEX_SETTINGS_FILE}"}
    ```

    <Note>
      Pass `--codex` to configure Codex or `--claude-desktop` to configure [Claude Desktop (Cowork 3P)](/docs/ai-gateway/claude-desktop) instead of `--claude-code`, or omit all of them to auto-detect which tools are installed and configure each one.
    </Note>

    | Path                                       | Owner        | Mode           | Effect                                          |
    | ------------------------------------------ | ------------ | -------------- | ----------------------------------------------- |
    | `/Library/Application Support/ClaudeCode/` | `root:wheel` | `755`          | Users can read but not write                    |
    | `managed-settings.json`                    | `root:wheel` | `644` + `schg` | Locked — root must run `chflags noschg` to edit |
  </Tab>

  <Tab title="Linux">
    Run via your Linux MDM or configuration management tool (Ansible, Chef, Puppet, etc.). Must run as root.

    ```bash theme={"dark"}
    #!/bin/bash
    # MDM Deployment Script: Claude Code — Linux (amd64)
    # Must be run as root.

    set -euo pipefail

    [[ "$(id -u)" -ne 0 ]] && { echo "ERROR: Must be run as root." >&2; exit 1; }

    CONFIG_FILE="/etc/tfy/tfy-local-ai-setup.conf"

    log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }

    # ---------------------------------------------------------------------------
    # Config — fill in on first run. Values are saved to ${CONFIG_FILE} and
    # reloaded automatically on subsequent runs — no edits needed after that.
    # To update a value later, set it here and run the script once more.
    # ---------------------------------------------------------------------------
    GATEWAY_URL="<your-gateway-url>"
    CONTROL_PLANE_URL="<your-control-plane-url>"
    TENANT_NAME="<your-tenant-name>"

    # Optional: per-tool gateway URL overrides (both default to GATEWAY_URL if not set)
    # CLAUDE_GATEWAY_URL=""
    # CODEX_GATEWAY_URL=""

    # Optional: override model IDs (defaults shown — change if using Claude Enterprise or a virtual model)
    # OPUS_MODEL="claude-code/claude-opus"
    # SONNET_MODEL="claude-code/claude-sonnet"
    # HAIKU_MODEL="claude-code/claude-haiku"

    # Optional: path to a JSON template for Claude Code managed-settings.json
    # SETTINGS_FILE="/etc/tfy/base-settings.json"

    # Optional: path to a TOML template for Codex config.toml
    # CODEX_SETTINGS_FILE="/etc/tfy/base-codex-config.toml"

    # ---------------------------------------------------------------------------
    # Load + save config (before binary install)
    # ---------------------------------------------------------------------------
    # Fill in any placeholder values from the saved config file
    if [[ -f "${CONFIG_FILE}" ]]; then
      while IFS='=' read -r _key _value; do
        [[ -z "${_key}" || "${_key}" == \#* ]] && continue
        _key="${_key// /}"; _value="${_value%\"}"; _value="${_value#\"}"
        case "${_key}" in
          GATEWAY_URL)       [[ "${GATEWAY_URL}"       == "<your-gateway-url>" ]]       && GATEWAY_URL="${_value}" ;;
          CONTROL_PLANE_URL) [[ "${CONTROL_PLANE_URL}" == "<your-control-plane-url>" ]] && CONTROL_PLANE_URL="${_value}" ;;
          TENANT_NAME)       [[ "${TENANT_NAME}"       == "<your-tenant-name>" ]]       && TENANT_NAME="${_value}" ;;
        esac
      done < "${CONFIG_FILE}"
    fi

    [[ "${GATEWAY_URL}"       == "<your-gateway-url>" ]]       && { log "ERROR: GATEWAY_URL not set — fill it in above or check ${CONFIG_FILE}."; exit 1; }
    [[ "${CONTROL_PLANE_URL}" == "<your-control-plane-url>" ]] && { log "ERROR: CONTROL_PLANE_URL not set — fill it in above or check ${CONFIG_FILE}."; exit 1; }
    [[ "${TENANT_NAME}"       == "<your-tenant-name>" ]]       && { log "ERROR: TENANT_NAME not set — fill it in above or check ${CONFIG_FILE}."; exit 1; }

    # Persist config for future runs (root-owned, 644 — developers cannot edit)
    mkdir -p "$(dirname "${CONFIG_FILE}")"
    cat > "${CONFIG_FILE}" <<CONF
    GATEWAY_URL="${GATEWAY_URL}"
    CONTROL_PLANE_URL="${CONTROL_PLANE_URL}"
    TENANT_NAME="${TENANT_NAME}"
    CONF
    chown root:root "${CONFIG_FILE}" && chmod 644 "${CONFIG_FILE}"
    log "Config saved to ${CONFIG_FILE}."

    BINARY_PATH="/usr/local/bin/tfy-local-ai-setup"
    RELEASE_TAG="v1.3.0"
    BINARY_URL="https://github.com/truefoundry/tfy-local-ai-setup/releases/download/${RELEASE_TAG}/tfy-local-ai-setup-linux-amd64"
    VERSION_FILE="${BINARY_PATH}.version"

    # ---------------------------------------------------------------------------
    # Install binary (skip if already on the correct release tag)
    # ---------------------------------------------------------------------------
    INSTALLED_TAG="$([[ -f "${VERSION_FILE}" ]] && cat "${VERSION_FILE}" || echo '')"

    if [[ ! -f "${BINARY_PATH}" ]] || [[ "${INSTALLED_TAG}" != "${RELEASE_TAG}" ]]; then
      log "Installing tfy-local-ai-setup ${RELEASE_TAG} (linux-amd64)..."
      curl -fsSL "${BINARY_URL}" -o "${BINARY_PATH}" && chmod +x "${BINARY_PATH}"
      echo "${RELEASE_TAG}" > "${VERSION_FILE}"
      log "tfy-local-ai-setup installed at ${BINARY_PATH}."
    else
      log "tfy-local-ai-setup ${RELEASE_TAG} already installed — skipping download."
    fi

    # ---------------------------------------------------------------------------
    # Run
    # ---------------------------------------------------------------------------
    exec "${BINARY_PATH}" \
      --claude-code \
      --url="${CONTROL_PLANE_URL}" \
      --tenant="${TENANT_NAME}" \
      --gateway="${GATEWAY_URL}" \
      ${CLAUDE_GATEWAY_URL:+--claude-gateway="${CLAUDE_GATEWAY_URL}"} \
      ${CODEX_GATEWAY_URL:+--codex-gateway="${CODEX_GATEWAY_URL}"} \
      ${OPUS_MODEL:+--opus-model="${OPUS_MODEL}"} \
      ${SONNET_MODEL:+--sonnet-model="${SONNET_MODEL}"} \
      ${HAIKU_MODEL:+--haiku-model="${HAIKU_MODEL}"} \
      ${SETTINGS_FILE:+--settings-file="${SETTINGS_FILE}"} \
      ${CODEX_SETTINGS_FILE:+--codex-settings-file="${CODEX_SETTINGS_FILE}"}
    ```

    <Note>
      Pass `--codex` to configure Codex or `--claude-desktop` to configure [Claude Desktop (Cowork 3P)](/docs/ai-gateway/claude-desktop) instead of `--claude-code`, or omit all of them to auto-detect which tools are installed and configure each one.
    </Note>

    | Path                    | Owner       | Mode                | Effect                                        |
    | ----------------------- | ----------- | ------------------- | --------------------------------------------- |
    | `/etc/claude-code/`     | `root:root` | `755`               | Users can read but not write                  |
    | `managed-settings.json` | `root:root` | `644` + `chattr +i` | Immutable — root must run `chattr -i` to edit |
  </Tab>

  <Tab title="Windows">
    Run via Intune, SCCM, or Group Policy as Administrator. This is a PowerShell script.

    ```powershell theme={"dark"}
    # MDM Deployment Script: Claude Code — Windows (amd64)
    # Must be run as Administrator.

    #Requires -RunAsAdministrator
    Set-StrictMode -Version Latest
    $ErrorActionPreference = "Stop"

    $ConfigFile = "C:\ProgramData\TrueFoundry\tfy-local-ai-setup.conf"

    function Write-Log {
      param([string]$Message)
      Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $Message"
    }

    # ---------------------------------------------------------------------------
    # Config — fill in on first run. Values are saved to $ConfigFile and
    # reloaded automatically on subsequent runs — no edits needed after that.
    # To update a value later, set it here and run the script once more.
    # ---------------------------------------------------------------------------
    $GatewayUrl      = "<your-gateway-url>"
    $ControlPlaneUrl = "<your-control-plane-url>"
    $TenantName      = "<your-tenant-name>"

    # Optional overrides — leave empty ("") to use defaults.
    $ClaudeGatewayUrl  = ""   # per-tool gateway for Claude Code (defaults to $GatewayUrl)
    $CodexGatewayUrl   = ""   # per-tool gateway for Codex (defaults to $GatewayUrl)
    $OpusModel         = ""   # defaults to claude-code/claude-opus
    $SonnetModel       = ""   # defaults to claude-code/claude-sonnet
    $HaikuModel        = ""   # defaults to claude-code/claude-haiku
    $SettingsFile      = ""   # JSON template for Claude Code managed-settings.json
    $CodexSettingsFile = ""   # TOML template for Codex config.toml

    # ---------------------------------------------------------------------------
    # Load + save config (before binary install)
    # ---------------------------------------------------------------------------
    # Fill in any placeholder values from the saved config file
    if (Test-Path $ConfigFile) {
      Get-Content $ConfigFile | ForEach-Object {
        if ($_ -match '^([A-Z_]+)="(.*)"$') {
          $k = $Matches[1]; $v = $Matches[2]
          switch ($k) {
            "GATEWAY_URL"       { if ($GatewayUrl      -eq "<your-gateway-url>")       { $GatewayUrl      = $v } }
            "CONTROL_PLANE_URL" { if ($ControlPlaneUrl -eq "<your-control-plane-url>") { $ControlPlaneUrl = $v } }
            "TENANT_NAME"       { if ($TenantName      -eq "<your-tenant-name>")       { $TenantName      = $v } }
          }
        }
      }
    }

    if ($GatewayUrl      -eq "<your-gateway-url>")       { Write-Error "GatewayUrl not set — fill it in above or check $ConfigFile"; exit 1 }
    if ($ControlPlaneUrl -eq "<your-control-plane-url>") { Write-Error "ControlPlaneUrl not set — fill it in above or check $ConfigFile"; exit 1 }
    if ($TenantName      -eq "<your-tenant-name>")       { Write-Error "TenantName not set — fill it in above or check $ConfigFile"; exit 1 }

    # Persist config for future runs (SYSTEM-owned, Administrators + Users: read-only)
    $ConfigDir = Split-Path $ConfigFile
    if (-not (Test-Path $ConfigDir)) { New-Item -ItemType Directory -Path $ConfigDir | Out-Null }
    @"
    GATEWAY_URL="$GatewayUrl"
    CONTROL_PLANE_URL="$ControlPlaneUrl"
    TENANT_NAME="$TenantName"
    "@ | Set-Content -Path $ConfigFile -Encoding UTF8
    icacls $ConfigFile /inheritance:r /grant "SYSTEM:(F)" /grant "Administrators:(R)" /grant "Users:(R)" | Out-Null
    Write-Log "Config saved to $ConfigFile."

    $BinaryDir   = "C:\Program Files\TrueFoundry"
    $BinaryPath  = "$BinaryDir\tfy-local-ai-setup.exe"
    $ReleaseTag  = "v1.3.0"
    $BinaryUrl   = "https://github.com/truefoundry/tfy-local-ai-setup/releases/download/$ReleaseTag/tfy-local-ai-setup-windows-amd64.exe"
    $VersionFile = "$BinaryPath.version"

    # ---------------------------------------------------------------------------
    # Install binary (skip if already on the correct release tag)
    # ---------------------------------------------------------------------------
    $InstalledTag = if (Test-Path $VersionFile) { (Get-Content $VersionFile -Raw).Trim() } else { "" }

    if (-not (Test-Path $BinaryPath) -or $InstalledTag -ne $ReleaseTag) {
      Write-Log "Installing tfy-local-ai-setup $ReleaseTag (windows-amd64)..."
      if (-not (Test-Path $BinaryDir)) { New-Item -ItemType Directory -Path $BinaryDir | Out-Null }
      Invoke-WebRequest -Uri $BinaryUrl -OutFile $BinaryPath -UseBasicParsing
      Set-Content -Path $VersionFile -Value $ReleaseTag
      Write-Log "tfy-local-ai-setup installed at $BinaryPath."
    } else {
      Write-Log "tfy-local-ai-setup $ReleaseTag already installed — skipping download."
    }

    # ---------------------------------------------------------------------------
    # Run
    # ---------------------------------------------------------------------------
    $RunArgs = @("--claude-code", "--url=$ControlPlaneUrl", "--tenant=$TenantName", "--gateway=$GatewayUrl")
    if ($ClaudeGatewayUrl) { $RunArgs += "--claude-gateway=$ClaudeGatewayUrl" }
    if ($CodexGatewayUrl)  { $RunArgs += "--codex-gateway=$CodexGatewayUrl" }
    if ($OpusModel)          { $RunArgs += "--opus-model=$OpusModel" }
    if ($SonnetModel)        { $RunArgs += "--sonnet-model=$SonnetModel" }
    if ($HaikuModel)         { $RunArgs += "--haiku-model=$HaikuModel" }
    if ($SettingsFile)       { $RunArgs += "--settings-file=$SettingsFile" }
    if ($CodexSettingsFile)  { $RunArgs += "--codex-settings-file=$CodexSettingsFile" }
    & $BinaryPath @RunArgs
    if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
    ```

    | Path                           | Permissions                              | Effect                                                            |
    | ------------------------------ | ---------------------------------------- | ----------------------------------------------------------------- |
    | `C:\Program Files\ClaudeCode\` | SYSTEM: Full, Admins: Read, Users: Read  | Users cannot write to the directory                               |
    | `managed-settings.json`        | ACL: SYSTEM full, Admins/Users read-only | Re-run script to update; `icacls` grants write back automatically |
  </Tab>
</Tabs>

<Note>
  Prefer a server-side option? Claude's Admin Console (**Settings → Policies**) can push tool permissions, MCP allowlists, and model restrictions to all members without deploying files — useful for BYOD. Note that server-managed settings require a direct connection to `api.anthropic.com` and are bypassed when you route through a gateway via `ANTHROPIC_BASE_URL`.
</Note>

***

## Using Claude Code MDM with a Claude Enterprise Account

If your organization has a **Claude Enterprise subscription** (rather than a direct Anthropic API key), the MDM setup requires one additional configuration step on TrueFoundry: you create an Anthropic provider that acts as a pass-through, and the authentication is supplied at request time by each developer's own Claude Enterprise account — not stored as a credential on TrueFoundry.

This approach also gives you **per-user usage attribution on both sides** — TrueFoundry logs which user made each request, and Anthropic attributes usage to the individual Claude Enterprise seat. This works because the authorization token is user-specific: each developer obtains it by logging into their own Claude Enterprise account, so every request carries their identity end-to-end through the gateway.

<Warning>
  **First-time login required.** The first time a developer opens Claude Code on a managed machine, they will be prompted to log in to their Claude Enterprise account through a browser. This device-authorization flow is what binds the machine to the developer's identity and generates the initial refresh token. After that, the MDM script silently refreshes the token on every hourly run — no further interaction is needed unless the refresh token expires or the developer switches accounts.
</Warning>

<Steps>
  <Step title="Create an Anthropic provider on TrueFoundry">
    In the TrueFoundry platform, navigate to **Integrations → Providers** and create a new provider of type **Anthropic**.

    When prompted for authentication credentials, **leave the API key field empty**. Do not enter any API key or secret. The gateway will authenticate using the `X-TFY-API-KEY` token injected by the MDM script at the time of each request — no stored credential is needed or desired.
  </Step>

  <Step title="Copy the model IDs">
    After saving the provider, TrueFoundry assigns model IDs for each Claude model tier (for example, `claude-enterprise/claude-opus-4-6`, `claude-enterprise/claude-sonnet-4-6`, `claude-enterprise/claude-haiku-4-5`). Copy these model IDs from the provider detail page.
  </Step>

  <Step title="Update the MDM script config">
    In the MDM script, set the three config values to match your TrueFoundry setup:

    ```bash theme={"dark"}
    GATEWAY_URL="<your-truefoundry-gateway-url>"
    CONTROL_PLANE_URL="<your-truefoundry-control-plane-url>"
    TENANT_NAME="<your-tenant-name>"
    ```

    Then confirm the model env vars in the JSON block match the model IDs from the previous step:

    ```json theme={"dark"}
    "ANTHROPIC_DEFAULT_OPUS_MODEL":   "claude-enterprise/claude-opus-4-6",
    "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-enterprise/claude-sonnet-4-6",
    "ANTHROPIC_DEFAULT_HAIKU_MODEL":  "claude-enterprise/claude-haiku-4-5"
    ```
  </Step>
</Steps>

<Warning>
  The Anthropic provider on TrueFoundry **must have an empty API key**. If a stored API key is present, TrueFoundry will use it instead of the per-request token from the MDM script, which breaks the Claude Enterprise subscription flow and may route traffic under the wrong account.
</Warning>

<Note>
  The `tfy-local-ai-setup` binary fetches a fresh OAuth token on every MDM run and writes it into `ANTHROPIC_CUSTOM_HEADERS` as `X-TFY-API-KEY`. TrueFoundry's gateway forwards this header to Anthropic's API to authenticate each request against the developer's Claude Enterprise subscription. No long-lived credentials are stored anywhere on the managed machine.
</Note>

**Using a virtual model with Claude Enterprise**

The MDM setup works with [virtual models](/docs/ai-gateway/virtual-model) as well. Point the model IDs in the MDM script to a virtual model instead of a direct provider model — you get two immediate benefits:

* **Fallbacks** — if one target is unavailable, traffic automatically falls through to the next
* **Model changes without touching the MDM script** — update the virtual model's routing on TrueFoundry and the change takes effect immediately across all managed machines, with no script redeployment

One important constraint: **only one of the virtual model's targets can be a Claude Enterprise provider account**. The authorization header injected by the MDM script (`X-TFY-API-KEY`) is issued by Anthropic for a specific Claude Enterprise account. If multiple targets were Claude Enterprise providers, all of them would receive the same token — which is only valid for one account, causing the others to fail. Non-Claude-Enterprise targets (e.g. a fallback to a different provider) can coexist without issue.

**Troubleshooting: 401 / 403 errors**

If Claude Code returns a `401 Unauthorized` or `403 Forbidden` error, the session token has expired or was never initialized. The developer needs to re-authenticate manually:

1. Open Claude Code and run `/login`
2. When prompted to choose an authentication method, select **Claude Enterprise** (the first option)
3. Complete the browser login flow

Once logged in, the MDM script will automatically pick up the new refresh token on its next hourly run and keep it fresh without further manual steps.

***

## 5. Observability with TrueFoundry

When Claude traffic flows through the [TrueFoundry AI Gateway](/docs/ai-gateway/intro-to-llm-gateway), you get built-in observability across both LLM and MCP requests — no extra instrumentation.

**Request tracing** — every request is traced with full attribution (user, model, MCP server, tool).

<Frame>
  <img src="https://mintcdn.com/truefoundry/Kp2QhbaDlupVHMTP/images/docs/ai-gateway/enterprise-security-claude-request-tracing.png?fit=max&auto=format&n=Kp2QhbaDlupVHMTP&q=85&s=b136ef35a23a0044d10ef95b16af7d12" alt="Claude request logs dashboard showing API calls, response times, and model usage metrics" width="1024" height="202" data-path="images/docs/ai-gateway/enterprise-security-claude-request-tracing.png" />
</Frame>

**Metrics dashboard** — real-time visibility into model, MCP, and guardrail metrics.

<Frame>
  <img src="https://mintcdn.com/truefoundry/Kp2QhbaDlupVHMTP/images/docs/ai-gateway/enterprise-security-claude-metrics-dashboard.png?fit=max&auto=format&n=Kp2QhbaDlupVHMTP&q=85&s=741cf56c35870c05a1a8028486b35faa" alt="Claude monitoring dashboard displaying request metrics, token usage, failure rates, and performance trends" width="1024" height="544" data-path="images/docs/ai-gateway/enterprise-security-claude-metrics-dashboard.png" />
</Frame>

All traces export to any OTEL-compatible platform (Grafana, Datadog, Splunk) for your SIEM. [Learn more](/docs/ai-gateway/export-opentelemetry-data). To set spending guardrails, use the Gateway's [budget limits](/docs/ai-gateway/budgetlimiting) and [rate limits](/docs/ai-gateway/ratelimiting) per user and team.

***

## 6. Data retention and compliance

By default, Anthropic may retain prompts and outputs for safety and quality. Review your enterprise agreement for the full scope.

<AccordionGroup>
  <Accordion title="Retention settings">
    * **Web:** set retention to a maximum of 30 days at [Organization Settings → Data and Privacy](https://claude.ai/admin-settings/data-privacy-controls).
    * **CLI:** local transcripts are auto-deleted via `transcriptRetentionDays` in `managed-settings.json` (recommended 7–14 days).
    * **Zero Data Retention (ZDR):** an enterprise add-on requested through your Claude account team that stops Anthropic retaining prompts/outputs beyond serving the request. [Learn more](https://platform.claude.com/docs/en/build-with-claude/zero-data-retention)
  </Accordion>

  <Accordion title="SOC 2 / HIPAA / GDPR">
    Anthropic holds SOC 2 Type II (available under NDA). Your admin responsibilities:

    * **SOC 2** — document user provisioning/deprovisioning, retain audit logs 90+ days, and maintain a vendor risk assessment.
    * **HIPAA** — a ZDR addendum is **required before processing PHI** with any Claude interface; mandate human review of outputs involving patient data and keep a full audit trail.
    * **GDPR** — control data residency (AWS EU / Vertex with Private Service Connect), honor right-to-erasure via the Compliance API, and use `deny` rules to minimize PII.
  </Accordion>
</AccordionGroup>

***

## Frequently asked questions

<AccordionGroup>
  <Accordion title="Why route Claude Code through TrueFoundry instead of straight to Anthropic?">
    A direct connection gives you no central place to enforce policy. Routing through the TrueFoundry AI Gateway lets you apply access control, rate and budget limits, multi-provider routing with failover, and full tracing — across web and CLI — from a single control point, without changing client config.
  </Accordion>

  <Accordion title="How does authentication work without distributing API keys?">
    The MDM deployment installs the `tfy-local-ai-setup` binary and runs it on a schedule (hourly) to fetch a fresh gateway token scoped to the user's TrueFoundry identity. The token is written into `ANTHROPIC_CUSTOM_HEADERS` as `X-TFY-API-KEY` and refreshed automatically — no personal or long-lived API keys on developer machines.
  </Accordion>

  <Accordion title="What does the TrueFoundry MCP Gateway add over connecting MCP servers directly?">
    It centralizes MCP governance: a registry of approved servers, unified outbound authentication, role-based access, tool-level controls, guardrails, and a full audit trail of every tool call. You allowlist only the gateway URL, so developers can't connect to unvetted servers.
  </Accordion>

  <Accordion title="I accidentally closed the browser login window — do I have to wait for the next MDM sync?">
    No. The cancelled device code expires immediately, but you can start a fresh login at any time. Since your config is already saved from the initial run, source the config file and run the binary directly — no flags to look up:

    <Tabs>
      <Tab title="macOS">
        ```bash theme={"dark"}
        source /Library/Preferences/com.truefoundry.tfy-local-ai-setup.conf
        sudo /usr/local/bin/tfy-local-ai-setup \
          --url="${CONTROL_PLANE_URL}" \
          --tenant="${TENANT_NAME}" \
          --gateway="${GATEWAY_URL}"
        ```
      </Tab>

      <Tab title="Linux">
        ```bash theme={"dark"}
        source /etc/tfy/tfy-local-ai-setup.conf
        sudo /usr/local/bin/tfy-local-ai-setup \
          --url="${CONTROL_PLANE_URL}" \
          --tenant="${TENANT_NAME}" \
          --gateway="${GATEWAY_URL}"
        ```
      </Tab>

      <Tab title="Windows">
        Open **PowerShell as Administrator**, then run:

        ```powershell theme={"dark"}
        $c = @{}
        Get-Content "C:\ProgramData\TrueFoundry\tfy-local-ai-setup.conf" |
          Where-Object { $_ -match '^([A-Z_]+)="(.*)"$' } |
          ForEach-Object { $c[$Matches[1]] = $Matches[2] }
        & "C:\Program Files\TrueFoundry\tfy-local-ai-setup.exe" `
          --url="$($c.CONTROL_PLANE_URL)" `
          --tenant="$($c.TENANT_NAME)" `
          --gateway="$($c.GATEWAY_URL)"
        ```
      </Tab>
    </Tabs>

    Once you complete the login, the refresh token is saved and all future MDM runs will finish silently — no browser prompt needed.
  </Accordion>

  <Accordion title="Can developers override these settings locally?">
    No. When `managed-settings.json` is deployed to the system path and locked (and `allowManagedPermissionRulesOnly` is set), it takes highest priority and cannot be overridden by project- or user-level settings.
  </Accordion>

  <Accordion title="A browser login window opens every time the MDM script runs — is this expected?">
    No, this should only happen on the **first run** or when the cached refresh token at `~/.tf/refresh-token` has expired. Once the developer completes the login, subsequent MDM runs silently refresh the token without any browser prompt. If the window keeps appearing on every run, the refresh token is not being saved — run the setup manually (see the FAQ above) to re-authenticate and reset the token.
  </Accordion>
</AccordionGroup>

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "TechArticle",
headline: "Securing Claude Code with TrueFoundry",
description:
  "Govern Claude Code in the enterprise: route model and MCP traffic through TrueFoundry and enforce settings across developer machines with MDM.",
mainEntityOfPage: {
  "@type": "WebPage",
  "@id":
    "https://www.truefoundry.com/docs/ai-gateway/mcp/enterprise-security-claude"
},
author: {
  "@type": "Organization",
  name: "TrueFoundry"
},
publisher: {
  "@type": "Organization",
  name: "TrueFoundry",
  logo: {
    "@type": "ImageObject",
    url: "https://cdn.prod.website-files.com/6291b38507a5238373237679/6291e20016f0c749e47497d5_logo-header.avif"
  }
},
dateModified: "2026-06-17",
keywords: ["Claude Code enterprise security", "Securing Claude with TrueFoundry"]
})
}}
/>

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: [
  {
    "@type": "Question",
    name: "Why route Claude Code through TrueFoundry instead of straight to Anthropic?",
    acceptedAnswer: {
      "@type": "Answer",
      text: "A direct connection gives you no central place to enforce policy. Routing through the TrueFoundry AI Gateway lets you apply access control, rate and budget limits, multi-provider routing with failover, and full tracing across web and CLI from a single control point, without changing client config."
    }
  },
  {
    "@type": "Question",
    name: "How does authentication work without distributing API keys?",
    acceptedAnswer: {
      "@type": "Answer",
      text: "The MDM deployment installs the tfy-local-ai-setup binary and runs it on a schedule (hourly) to fetch a fresh gateway token scoped to the user's TrueFoundry identity. The token is written into ANTHROPIC_CUSTOM_HEADERS as X-TFY-API-KEY and refreshed automatically, so there are no personal or long-lived API keys on developer machines."
    }
  },
  {
    "@type": "Question",
    name: "What does the TrueFoundry MCP Gateway add over connecting MCP servers directly?",
    acceptedAnswer: {
      "@type": "Answer",
      text: "It centralizes MCP governance: a registry of approved servers, unified outbound authentication, role-based access, tool-level controls, guardrails, and a full audit trail of every tool call. You allowlist only the gateway URL, so developers cannot connect to unvetted servers."
    }
  },
  {
    "@type": "Question",
    name: "Can developers override these settings locally?",
    acceptedAnswer: {
      "@type": "Answer",
      text: "No. When managed-settings.json is deployed to the system path and locked (and allowManagedPermissionRulesOnly is set), it takes highest priority and cannot be overridden by project- or user-level settings."
    }
  }
]
})
}}
/>

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org/",
"@type": "BreadcrumbList",
itemListElement: [
  {
    "@type": "ListItem",
    position: 1,
    name: "Home",
    item: "https://www.truefoundry.com"
  },
  {
    "@type": "ListItem",
    position: 2,
    name: "Truefoundry Docs",
    item: "https://www.truefoundry.com/docs/create-and-setup-your-account"
  },
  {
    "@type": "ListItem",
    position: 3,
    name: "AI/LLM Gateway Doc",
    item: "https://www.truefoundry.com/docs/ai-gateway/"
  },
  {
    "@type": "ListItem",
    position: 4,
    name: "MCP Gateway Doc",
    item: "https://www.truefoundry.com/docs/ai-gateway/mcp/mcp-overview"
  },
  {
    "@type": "ListItem",
    position: 5,
    name: "Claude Enterprise Security",
    item: "https://www.truefoundry.com/docs/ai-gateway/mcp/enterprise-security-claude"
  }
]
})
}}
/>

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
name: "TrueFoundry",
url: "https://www.truefoundry.com/"
})
}}
/>
