Claude Code on Microsoft Foundry: server setup
Run Claude Code on Microsoft Foundry from a VPS or CI runner with no browser: API key or service principal auth, model pinning, and the errors the docs list.
Claude Code on Microsoft Foundry: what you are setting up
Claude Code on Microsoft Foundry needs no login flow at all: you set CLAUDE_CODE_USE_FOUNDRY=1, name your Azure resource, give it a credential, and pin three deployment names. Every one of those is an environment variable, so a VPS or a CI runner with no browser is the easy case, not the hard one. The only parts that need a browser happen once, in the Foundry portal, on your laptop.
Foundry is the third provider path for Claude Code, next to Amazon Bedrock and Google Cloud. If you have already followed running Claude Code through Bedrock or Vertex, the shape is the same: a CLAUDE_CODE_USE_* switch and a handful of provider variables that name your credentials and your deployments. What differs is that Foundry has no setup wizard, and the credential chain behaves differently on a machine that has never run az login. Those two things are most of this guide.
As of September 2026 the Claude Code docs describe five steps for Foundry. They are covered here in order, and at each one the part that a headless server changes is called out.
Step 1: create the resource and one deployment per model
In the Foundry portal create a Foundry resource and write down its name. That name becomes {resource} in the endpoint https://{resource}.services.ai.azure.com/anthropic/v1/*. Claude Code builds the URL from it, so a typo here shows up later as a connection error rather than an authentication error.
Then create one deployment for each model family Claude Code uses: Claude Opus, Claude Sonnet and Claude Haiku. Foundry separates the two levels. A resource holds the security and billing settings, and a deployment is the model instance you call. The deployment name defaults to the model ID, for example claude-sonnet-5, and you can rename it. It cannot be changed after creation, and it is the string you will put in the model variables in step 4, so write each one down.
Two settings on the deployment form matter later. Under Model version settings, pick a specific version rather than the auto-update option. The Claude Code docs are direct about why: "Pin specific model versions for every deployment. Without pinning, model aliases such as sonnet and opus resolve to Claude Code's built-in default for Microsoft Foundry, which can lag the newest release and may not yet be available in your account. Microsoft Foundry has no startup model check, so requests fail when the default is unavailable." The second setting is the hosting option, covered next.
Hosted on Azure or hosted on Anthropic: where your prompts go
Each Claude deployment in Foundry has a hosting option, chosen when you create it. Both are operated by Anthropic. The difference is whose hardware runs the inference, and that decides where your prompts are processed.
Hosted on Azure runs the model on Azure infrastructure. It offers the latest Opus, Sonnet and Haiku models, and it is the only option that supports a US Data Zone Standard deployment, which keeps inference inside the United States. The data residency statement in Anthropic's docs is worth quoting exactly: "For deployments hosted on Azure, prompts and completions remain within Azure. Only usage metadata and content flagged by Anthropic's safety systems egress to Anthropic." If your reason for going through Azure is a compliance rule about where prompts are processed, this is the option that satisfies it.
Hosted on Anthropic runs the same Anthropic-operated service on Anthropic infrastructure. It exposes every Claude model that Foundry lists, including older versions and models that have not reached Azure hosting yet, and it supports features that the Azure-hosted option does not: code execution, Agent Skills, programmatic tool calling, the Files API, and the newer web search and web fetch tool versions. Prompts sent to this option leave Azure.
For Claude Code, the hosted-on-Azure option is fine. Requests that use unsupported features against an Azure-hosted deployment return 400 Bad Request by design, but the docs also say "Claude Code detects deployments hosted on Azure and automatically adapts its feature set", so the client does not send them. Choose the option by your data rule, not by the feature list.
Step 2: pick one of the three authentication methods
Claude Code checks for credentials in a fixed order. ANTHROPIC_FOUNDRY_AUTH_TOKEN wins if it is set. Otherwise ANTHROPIC_FOUNDRY_API_KEY is used. If neither is set, Claude Code falls through to the Azure SDK default credential chain. The docs state the precedence in one line: "ANTHROPIC_FOUNDRY_AUTH_TOKEN takes precedence over ANTHROPIC_FOUNDRY_API_KEY and over the default credential chain."
Option A: the API key, which is the right answer for a single box
In the portal, open your resource, go to Endpoints and keys, and copy the API key. On the server:
export ANTHROPIC_FOUNDRY_API_KEY=your-azure-api-keyThat is the whole method. The key does not expire on a timer and needs no tenant or client IDs. There is no credential chain, so nothing probes for tools that are not installed. Before you touch Claude Code, prove the key works with a direct request. Replace my-claude-resource with your resource name and claude-sonnet-5 with one of your deployment names:
curl -s https://my-claude-resource.services.ai.azure.com/anthropic/v1/messages \
-H "content-type: application/json" \
-H "api-key: $ANTHROPIC_FOUNDRY_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-d '{"model": "claude-sonnet-5", "max_tokens": 64,
"messages": [{"role": "user", "content": "Say ok."}]}'A healthy result is a JSON body with "type":"message" and a usage object. A 401 Unauthorized means the key is wrong, and a Deployment not found error means the deployment name is wrong. Fix both here, where the error is one line, rather than inside Claude Code.
The cost of the simplicity is that the key is a bearer secret for the whole resource. Anyone holding it can spend against your Azure subscription, with no per-user identity in the logs. Keep it in a file with mode 600, not in a shell history line, and rotate it from the portal if the box is ever compromised. For one VPS or one CI runner, that trade is fine. The moment you have several machines or several people, the second option becomes worth its setup cost.
Option B: the default credential chain, on a machine with no browser
When neither key variable is set, Claude Code hands authentication to the Azure Identity library's DefaultAzureCredential. The docs describe it as supporting "a variety of methods for authenticating local and remote workloads", and the example they give is az login. On a laptop that is the whole story. On a server it is the wrong mental model, because az login opens a browser, and the chain was designed to work without one.
The chain tries credentials in a fixed order and stops at the first that returns a token: environment variables, then workload identity, then managed identity, then the developer tools (VS Code, Azure CLI, Azure PowerShell, Azure Developer CLI, and a broker). The first three are the server-side entries. What that means on a headless box:
- A service principal in environment variables is the general case. The
EnvironmentCredentialentry readsAZURE_TENANT_ID,AZURE_CLIENT_IDandAZURE_CLIENT_SECRET(orAZURE_CLIENT_CERTIFICATE_PATHin place of the secret). Set those and the chain succeeds on its first entry, with noazbinary on the box at all. - A managed identity works only when the server is an Azure resource: a VM, a scale set, AKS, App Service, Azure Functions or Azure Arc. The credential fetches a token from the instance metadata endpoint at
169.254.169.254. On a VPS from any other provider that address is not there, the probe fails, and the library's own debug log shows it moving on:ManagedIdentityCredential - IMDS: Caught error RestError: connect ENETUNREACH 169.254.169.254:80. On an Azure VM with a system-assigned identity you set nothing. With a user-assigned identity, setAZURE_CLIENT_IDto that identity's client ID. - The developer tools cannot succeed on a server. No
azmeans noAzureCliCredential, and noazdmeans noAzureDeveloperCliCredential. Each one fails in turn before the chain gives up.
To create the service principal, run this once on a machine that has az and a logged-in session. Scope it to the one resource, with a role that can call the models. The inner az cognitiveservices account show looks up the resource ID for the scope, so replace my-claude-resource and my-resource-group with your resource name and its resource group. The Claude Code docs name the roles: "The Azure AI User and Cognitive Services User default roles include all required permissions for invoking Claude models."
az ad sp create-for-rbac --name claude-code-vps \
--role "Cognitive Services User" \
--scopes "$(az cognitiveservices account show --name my-claude-resource --resource-group my-resource-group --query id -o tsv)"The output is JSON with appId, password and tenant. Those map to the three variables: tenant is AZURE_TENANT_ID, appId is AZURE_CLIENT_ID and password is AZURE_CLIENT_SECRET. On the server:
export AZURE_TENANT_ID=your-tenant-id
export AZURE_CLIENT_ID=your-app-id
export AZURE_CLIENT_SECRET=your-client-secretProve the principal can get a token before you involve Claude Code. This is the standard client-credentials request to Entra ID, and it needs only curl:
curl -s -X POST "https://login.microsoftonline.com/$AZURE_TENANT_ID/oauth2/v2.0/token" \
-d "client_id=$AZURE_CLIENT_ID" \
-d "client_secret=$AZURE_CLIENT_SECRET" \
-d "scope=https://ai.azure.com/.default" \
-d "grant_type=client_credentials"A healthy result contains "token_type":"Bearer" and an access_token. An AADSTS7000215 error means the secret is wrong, and AADSTS700016 means the application ID does not exist in that tenant. If the token comes back but a Foundry request with it returns 403 Forbidden, the principal has no role on the resource, which means the RBAC assignment is missing or scoped to the wrong object.
Compared with the API key, this gives you a named identity in the Entra sign-in logs and a role you can scope and revoke. It costs three variables instead of one. The library refreshes the token for you as it expires, and the first request is slower while the chain tries its entries in turn.
Option C: a bearer token that something else obtained
ANTHROPIC_FOUNDRY_AUTH_TOKEN is for the case where a wrapper has already done the Entra work. The docs: "Claude Code sends the value of ANTHROPIC_FOUNDRY_AUTH_TOKEN on every request as the Authorization: Bearer header. Use this option when another process, such as a host application or a sign-in script, has already obtained an access token for you. Requires Claude Code v2.1.203 or later."
export ANTHROPIC_FOUNDRY_AUTH_TOKEN=your-entra-access-tokenThe access_token from the curl above is exactly this kind of token, and on a laptop az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv prints one. The catch for a long-running server is that Claude Code does not refresh it. Anthropic's Foundry docs say tokens "typically expire after 1 hour", so a session that outlives the token starts failing with 401 Unauthorized. Use this for a CI job that runs for minutes, with the token fetched at the top of the job. For a box that stays up, use option A or B.
Step 3: point Claude Code at Foundry
Two variables turn the provider on. The first is the switch, the second names the resource:
export CLAUDE_CODE_USE_FOUNDRY=1
export ANTHROPIC_FOUNDRY_RESOURCE=my-claude-resourceIf your endpoint is not the standard shape, for example behind a private endpoint or an API gateway, set ANTHROPIC_FOUNDRY_BASE_URL to the full base URL instead: the scheme, the host, and the /anthropic path with nothing after it. For the standard endpoint that is the URL the curl test in step 2 used, minus /v1/messages. The two are alternatives, and ANTHROPIC_FOUNDRY_RESOURCE is enough for most people.
Set the resource name to the real thing. The docs' second troubleshooting entry exists because of placeholders: "Check that ANTHROPIC_FOUNDRY_RESOURCE is set to your actual resource name rather than a placeholder. Claude Code builds the endpoint URL from this value, so an incorrect name points at a host that doesn't exist."
Step 4: pin the deployment names
Set the three model variables to the deployment names from step 1. If you kept the default names, they match the model IDs:
export ANTHROPIC_DEFAULT_OPUS_MODEL='claude-opus-4-8'
export ANTHROPIC_DEFAULT_SONNET_MODEL='claude-sonnet-5'
export ANTHROPIC_DEFAULT_HAIKU_MODEL='claude-haiku-4-5'Two of the gotchas in the docs live here, and both are silent unless you know to look. The first is the opus alias: "Without ANTHROPIC_DEFAULT_OPUS_MODEL, the opus alias on Microsoft Foundry resolves to Opus 4.6." If you deployed Opus 4.8 or Opus 5 and never set the variable, /model opus either fails because no claude-opus-4-6 deployment exists in your resource, or succeeds against an older deployment you forgot about. The second is the Haiku deployment: "Background tasks such as session title generation use the small/fast model, normally a Haiku-class model. On Microsoft Foundry, Claude Code defaults this to the primary model because not every account has a Haiku deployment." So a box with no Haiku deployment and no ANTHROPIC_DEFAULT_HAIKU_MODEL pays Opus or Sonnet rates for every title and summary it generates. Deploy Haiku, then set the variable.
Prompt caching is on by default. If your sessions idle for more than five minutes between turns, which describes most interactive work on a remote box, the one-hour cache TTL keeps the cached context alive:
export ENABLE_PROMPT_CACHING_1H=1Cache writes at the one-hour TTL are billed at a higher rate than five-minute writes, so it is not free. The prompt caching break-even works through when the longer TTL pays for itself and when it costs you.
Step 5: run it, and check /status
cd ~/your-project
claudeThere is no wizard and nothing to click. The docs: "Unlike Amazon Bedrock and Google Cloud's Agent Platform, Microsoft Foundry has no interactive setup wizard, so the environment variables in steps 3 and 4 are the only configuration path." Claude Code reads the variables at start and connects on the first prompt.
Verify with /status inside the session. The docs say what to expect: "The API provider line shows Microsoft Foundry, along with the resource name or base URL you configured." If that line says Anthropic instead, CLAUDE_CODE_USE_FOUNDRY was not in the environment of the shell that launched claude. That happens more often than it should on servers, and the next section is about it.
Note also what is missing. "When using Microsoft Foundry, the /logout command is unavailable since authentication is handled through Azure credentials." There is no session to log out of. To revoke access, rotate the API key in the portal, or delete the service principal's secret. A Foundry box never holds a claude.ai subscription either, which is a different pricing model entirely, and the subscription-versus-API-key comparison explains what you give up and gain by paying per token through Azure instead.
Keeping the variables alive on a server
An export typed at the prompt dies with the shell. Three places survive it.
Put the non-secret variables in ~/.claude/settings.json under the env key. Claude Code applies that block to every session it starts, whichever shell launched it:
{
"env": {
"CLAUDE_CODE_USE_FOUNDRY": "1",
"ANTHROPIC_FOUNDRY_RESOURCE": "my-claude-resource",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-8",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-5",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5"
}
}Put the secret in its own file, owned by the user who runs Claude Code, and source it from the shell profile:
mkdir -p ~/.config
install -m 600 /dev/null ~/.config/foundry.env
printf 'export ANTHROPIC_FOUNDRY_API_KEY=%s\n' 'your-azure-api-key' > ~/.config/foundry.env
echo '[ -r ~/.config/foundry.env ] && . ~/.config/foundry.env' >> ~/.bashrcFor a service principal, the same file holds AZURE_TENANT_ID, AZURE_CLIENT_ID and AZURE_CLIENT_SECRET instead.
The tmux detail: a new tmux window runs a fresh login shell, so it reads .bashrc and picks up the file above, but it does not inherit a variable you exported at the prompt of your SSH session before tmux attach. That is why the profile file is the right place and a one-off export is not. If you are running Claude Code in tmux on a VPS so sessions survive a dropped SSH connection, this is the one place the Foundry setup interacts with that pattern. For a cron job or a systemd unit, .bashrc is not read at all, so point EnvironmentFile= at the same file instead.
In CI, set the variables as job secrets and export them at the top of the job. claude -p "..." reads them the same way an interactive session does.
The two errors the docs list, and what they mean
Failed to get token from azureADTokenProvider: ChainedTokenCredential authentication failed. The chain tried every entry and none returned a token. The docs' fix: "Configure Entra ID on the environment, or set ANTHROPIC_FOUNDRY_API_KEY." On a server this almost always means neither key variable was set and the service principal variables were absent or misspelled. Check with env | grep -E 'ANTHROPIC_FOUNDRY|AZURE_' in the same shell that runs claude. A missing AZURE_TENANT_ID is enough to make EnvironmentCredential skip itself, and then the chain has nothing left on a non-Azure box.
Repeated connection errors on the first prompt. Claude Code started, /status even showed Foundry, and the first message failed several times. The docs: "Check that ANTHROPIC_FOUNDRY_RESOURCE is set to your actual resource name rather than a placeholder." The client built https://<whatever-you-set>.services.ai.azure.com and DNS has no such host. Confirm with getent hosts my-claude-resource.services.ai.azure.com using your real name. No output means the name is wrong.
Beyond those two, the errors are ordinary HTTP. 401 is a bad key or an expired bearer token, 403 is a principal with no role on the resource, 429 is the Azure rate limit for that deployment, and Deployment not found is a model variable that names something you did not create. Foundry does not return Anthropic's anthropic-ratelimit-* headers, so a 429 has to be diagnosed from Azure Monitor rather than from the response.
What it costs, and where to read the numbers
Foundry bills Claude usage through the Azure Marketplace in Claude Consumption Units, metered hourly and invoiced on the monthly Azure bill. No numbers here, because they change and because the exchange between units and tokens is Azure's to publish: the current figures are on Azure's AI Foundry pricing page. What does not change is the shape of the decision. Paying per token through a cloud marketplace is what you do when procurement or data residency demands it, and the Claude Code enterprise cost breakdown puts that path next to the subscription plans so you can see what the Azure route buys and what it gives up.
FAQ
Can I use Claude Code with Microsoft Foundry on a server without az login?
Yes. az login is one entry in the Azure default credential chain, not a requirement. On a headless server, either set ANTHROPIC_FOUNDRY_API_KEY and skip the chain entirely, or set AZURE_TENANT_ID, AZURE_CLIENT_ID and AZURE_CLIENT_SECRET for a service principal, which the chain picks up from the environment before it ever looks for the Azure CLI. On an Azure VM, a managed identity works with no variables at all.
Which credential wins if I set more than one?
ANTHROPIC_FOUNDRY_AUTH_TOKEN takes precedence over ANTHROPIC_FOUNDRY_API_KEY, and both take precedence over the default credential chain. A stale bearer token left in the environment overrides a valid API key, so if you get 401 errors with a key you know works, check for a forgotten ANTHROPIC_FOUNDRY_AUTH_TOKEN first.
Why does /logout say it is unavailable on Foundry?
Because there is no Claude account session to end. Authentication is handled through Azure credentials, so the command is disabled. To revoke access, regenerate the API key in the Foundry portal or delete the service principal's secret in Entra ID.
Why does /model opus give me an older model on Foundry?
Without ANTHROPIC_DEFAULT_OPUS_MODEL, the opus alias on Microsoft Foundry resolves to Claude Code's built-in default, which is Opus 4.6 as of September 2026. Foundry has no startup model check, so if no deployment by that name exists the request fails, and if one does exist you get the older model. Set ANTHROPIC_DEFAULT_OPUS_MODEL to the deployment name you created, and do the same for Sonnet and Haiku.
Do my prompts leave Azure when I use Foundry?
It depends on the hosting option you chose for the deployment. For deployments hosted on Azure, prompts and completions stay within Azure, and only usage metadata and content flagged by Anthropic's safety systems go to Anthropic. For deployments hosted on Anthropic, inference runs on Anthropic infrastructure, so prompts leave Azure. Pick hosted on Azure, and a US Data Zone deployment if you need inference kept inside the United States.