Authentication credentialsClaude Code authentication is handled through the platform's secure credential storage. On macOS, this generally means Keychain.
MCP secretsUse supported environment-variable or secret-injection mechanisms. Avoid hard-coding API keys or tokens in a shared .mcp.json.
Configuration filesFiles can contain server commands, arguments, environment-variable references and non-secret settings. A reference to a secret is not the secret itself.
Security rule: Never commit API keys, access tokens, passwords or private certificates to CLAUDE.md, settings.json or .mcp.json.
How to supply secrets safely, file by file
The principle is the same everywhere: the file holds a reference (a variable name, a helper script path or an OAuth registration), and the secret itself lives in the OS keychain, a secret manager or your shell environment.
| Where | Safe pattern | How |
CLAUDE.md | No secrets, ever. Name the variable instead. | Write “the API token is read from $SN_MCP_TOKEN”. CLAUDE.md is loaded into Claude's context, so anything in it is sent to the model. |
.mcp.json (local servers) | Environment-variable expansion | Use ${VAR} or ${VAR:-default} in a server's command, args, env, url or headers. Each teammate sets the variable on their own machine. |
.mcp.json (remote servers with OAuth) | Built-in OAuth 2.0 flow | Add the server without a token, then authenticate from /mcp or with claude mcp login <name>. Tokens are stored securely and refreshed automatically. A client secret passed with --client-secret is stored in the macOS Keychain or a credentials file, not in the config. |
.mcp.json (short-lived tokens, SSO, Kerberos) | headersHelper | Point headersHelper at a script that prints a JSON object of headers. It runs on every connection, has a 10-second timeout and is re-run automatically on a 401/403. |
settings.json (Claude API key) | apiKeyHelper | Point apiKeyHelper at a script that returns the key, for example from a vault. Use it for rotating credentials. |
settings.json env block | Non-secret values only | The env block is plain JSON. Don't put tokens in a committed .claude/settings.json. .claude/settings.local.json stays out of Git but is still plaintext on disk. |
Secret files in the repo (.env) | Deny rules | Add Read(./.env) and Read(./.env.*) to permissions.deny so Claude can't read them. |
| Claude Code login | Leave it to Claude Code | macOS: encrypted Keychain (falls back to ~/.claude/.credentials.json, mode 0600, if the Keychain rejects the write). Linux: ~/.claude/.credentials.json, mode 0600. Windows: %USERPROFILE%\.claude\.credentials.json. |
| CI pipelines | CI secret store and an environment variable | Run claude setup-token once to generate a one-year OAuth token, store it as a CI secret and expose it as CLAUDE_CODE_OAUTH_TOKEN. For API billing, use ANTHROPIC_API_KEY the same way. |
Step 1. Store the secret in the macOS Keychain (the trailing -w prompts for the value so it never appears in shell history).
security add-generic-password -a "$USER" -s sn-mcp-token -w
Step 2. Export it from the Keychain in ~/.zshrc, so only the variable name is ever written to any file.
export SN_MCP_TOKEN="$(security find-generic-password -s sn-mcp-token -w)"
Step 3. Reference the variable in the committed .mcp.json.
{
"mcpServers": {
"internal-api": {
"type": "http",
"url": "${INTERNAL_API_URL:-https://api.example.com}/mcp",
"headers": { "Authorization": "Bearer ${SN_MCP_TOKEN}" }
}
}
}
Step 4. Check it: claude mcp list warns about any variable that isn't set. Open a new terminal first, so ~/.zshrc is reloaded.
Dynamic headers for tokens that expire (the script must print JSON such as {"Authorization": "Bearer …"}):
"internal-api": {
"type": "http",
"url": "https://mcp.internal.example.com",
"headersHelper": "/opt/bin/get-mcp-auth-headers.sh"
}
Claude API key from a vault, plus deny rules for local secret files (~/.claude/settings.json):
{
"apiKeyHelper": "/Users/you/bin/get-anthropic-key.sh",
"permissions": {
"deny": ["Read(./.env)", "Read(./.env.*)"]
}
}
Pitfalls to know:
- If a referenced variable isn't set and has no default, the config still loads but the literal
${VAR} text is sent. claude mcp list reports a warning for that server.
- A variable set to an empty string counts as set, so
${VAR:-default} expands to empty, not to the default.
- In a remote server's
url and headers, known credential variables always read as empty, even when set, and a :-default on them is ignored. This covers Claude Code's own credentials (ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN), cloud provider credentials (AWS_BEARER_TOKEN_BEDROCK) and others such as HTTPS_PROXY and NPM_TOKEN. It stops a repository or plugin from sending them to a server it names. A name outside this set, such as API_KEY, expands normally, so copy a covered value into a variable with your own name.
- If a static
Authorization header is rejected, Claude Code reports the connection as failed and doesn't fall back to OAuth. Remove the header to use OAuth.
claude mcp add --env KEY=value sets environment variables for the server. As a precaution, don't pass a literal secret this way for a project-scoped server that will be committed. Use a ${VAR} reference in .mcp.json instead.