To run Claude Code on an Anthropic API key instead of a Pro or Max subscription, create a key in the Claude Console, export it as ANTHROPIC_API_KEY, start claude and approve the key when it asks. You then pay per token from your Console credit balance, and /status shows which credential is in use. The second half of this guide covers the other keys in your project (Stripe, OpenAI, database URLs) and how to keep them out of the agent's context.
The ways Claude Code can authenticate
The official authentication docs list several credential sources. These are the ones that matter for an individual developer or a small team:
| Method | How you set it | Who pays | Good for |
|---|---|---|---|
| claude.ai subscription | /login, choose your claude.ai account | Pro, Max, Ultra, Team or Enterprise plan | Daily interactive use |
| Claude Console login | /login, choose the Anthropic Console account | Console credits (API pricing) | Teams that want API billing without handling raw keys |
ANTHROPIC_API_KEY | Environment variable | Console credits | Scripts, claude -p, the Agent SDK |
apiKeyHelper | A command in settings.json that prints a key | Whoever owns the key | Keys from a vault or short-lived tokens |
CLAUDE_CODE_OAUTH_TOKEN | Token from claude setup-token | Your subscription | CI where a browser login isn't possible |
Environment keys and apiKeyHelper apply to the CLI and tools built on it (VS Code extension, Agent SDK, GitHub Actions). Claude Desktop and cloud sessions ignore them.
Set up Claude Code with an API key, step by step
- Create the key. Sign in to the Claude Console, open Settings > API keys and create a key. Add credits under Billing first, or the first request fails with
Credit balance is too low. - Export it in the shell that launches Claude Code. Keep it out of your project's files:
export ANTHROPIC_API_KEY="sk-ant-EXAMPLE-not-a-real-key" claude - Approve the key. In interactive mode Claude Code asks once whether to use the key and remembers the answer. To change it later, use the "Use custom API key" toggle in
/config. The toggle only shows while the variable is set. In non-interactive mode (claude -p) the key is always used when present. - Confirm. Run
/status. When both a login and a key are configured, it marks the credential that isn't in use.
Or let /login handle it
If your organization invited you to the Console, you don't need to copy a key at all. Choose the Anthropic Console account at the /login prompt and Claude Code offers two routes. Sign in with your Console account, marked recommended, keeps an OAuth token and creates no API key. Create an API key, marked legacy, creates a Console key and stores it with your other credentials. The keyless route needs Claude Code v2.1.242 or later, and the docs say to unset ANTHROPIC_API_KEY before you use it.
Which credential wins when several are set
This is where most "why am I being billed?" surprises come from. Claude Code picks the first match in this order:
- Cloud provider credentials, when
CLAUDE_CODE_USE_BEDROCK,CLAUDE_CODE_USE_VERTEXorCLAUDE_CODE_USE_FOUNDRYis set ANTHROPIC_AUTH_TOKEN(bearer token, for LLM gateways and proxies)ANTHROPIC_API_KEY(sent asX-Api-Key)apiKeyHelperoutputCLAUDE_CODE_OAUTH_TOKEN- Anthropic profile and federation credentials
- Subscription OAuth from
/login
The practical result: if you have a Max plan and an approved ANTHROPIC_API_KEY in your shell, the API key wins and the Console bills you.
Billing: subscription versus API key
- Subscription: usage is included in the plan, up to its limits. In
/usage, the session dollar figure isn't relevant for billing. You see plan usage bars instead. - API key or Console login: every token is billed from your Console balance at API prices.
/usageshows a session cost that Claude Code estimates locally from token counts at list price. Use the Console Cost page for real billing. - Spend control: the first time you authenticate with a Console account, a workspace called "Claude Code" is created to track usage. You can set workspace spend limits so one project can't drain the whole organization's balance.
Switching back to your subscription
# go back to the subscription for this shell
unset ANTHROPIC_API_KEY
claude
# inside Claude Code, check which login is active
/status
Also remove the export from ~/.zshrc or ~/.bashrc. /logout removes a saved credential entirely.
Troubleshooting
| Message | Usual cause | Fix |
|---|---|---|
Credit balance is too low | Console credits ran out, or an approved key is overriding your subscription | Check the API key row in /status. Unset the variable or add credits |
Invalid API key · Fix external API key | Typo, revoked key, or a stale key loaded by direnv, a dotenv shell plugin or your IDE terminal | Run env | grep ANTHROPIC in the same shell, then check the key in the Console |
Your apiKeyHelper script is failing | The helper exited with an error, printed nothing, or printed more than the key | Run the helper yourself. It must print only the key and exit 0. /login won't help while the setting is present |
All three come from the Claude Code error reference.
apiKeyHelper: fetch the key from a vault
An exported key sits in plain text in your shell profile and in the environment of every process the shell starts. The apiKeyHelper setting takes a shell command instead. Claude Code runs it, caches the output for five minutes by default (change it with CLAUDE_CODE_API_KEY_HELPER_TTL_MS), and runs it again after a 401 or 403. The same reference says env values in settings files are stored in plain text, and recommends apiKeyHelper for API credentials.
{
"apiKeyHelper": "envsave get es_EXAMPLE7a3f2b1e9c8d"
}
Any command that prints only the key works here, whether it comes from 1Password, Doppler or your own script. The envsave get example is from EnvSave, covered below. It writes only the secret value to stdout. The first read in a session asks for your master password or Touch ID, so unlock the vault before you start. If you put the helper in a project's .claude/settings.json, Claude Code doesn't run it until you accept the workspace trust prompt.
The other API keys in your project
Your Anthropic key is only one secret. A typical repository also has a Stripe secret key, an OpenAI key, a database URL and a webhook secret, often in a dotenv file at the root. Claude Code works with your files and your shell, so it can see them in several ways:
- File reads. If Claude opens your dotenv file to debug a config problem, its contents go into the conversation.
- Commands. Commands Claude runs inherit your shell environment. A
printenv, a debug log or a failing test that prints its config puts the values in the tool output. - Transcripts. Prompts and model outputs are sent to the API. Claude Code also keeps session transcripts in plain text under
~/.claude/projects/for 30 days by default (setcleanupPeriodDaysto change that). A key that shows up once in a session stays on disk for a month. - Prompt injection. Text in a README, an issue or a fetched web page can try to steer the agent. The security docs describe the safeguards, but an agent that already holds your Stripe key in context is a better target than one that never saw it.
What Claude Code itself gives you
Read deny rules
Permission rules can block Claude's file tools from reading a path. This example is adapted from the settings docs:
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)"
]
}
}
Two details from the permissions reference are easy to miss. First, a .claudeignore file has no effect, so move its entries into Read deny rules. Second, deny rules cover the built-in file tools and Bash commands Claude Code recognizes, such as cat, head and sed. They don't cover a command that reads files without naming them, like grep -r pattern ., or a Python or Node script that opens the file itself.
The sandbox
For enforcement at the operating-system level, turn on the sandbox with /sandbox or "sandbox": { "enabled": true }. It uses Seatbelt on macOS and bubblewrap on Linux and WSL2. Native Windows runs commands without it. The sandbox.credentials block can deny reads of credential files and unset environment variables before each sandboxed command:
{
"sandbox": {
"enabled": true,
"credentials": {
"files": [
{ "path": "~/.aws/credentials", "mode": "deny" }
],
"envVars": [
{ "name": "STRIPE_SECRET_KEY", "mode": "deny" },
{ "name": "OPENAI_API_KEY", "mode": "deny" }
]
}
}
}
There is also a "mask" mode. Sandboxed commands see a placeholder, and the sandbox proxy swaps in the real value only on requests to hosts you allow. It needs TLS termination and is honored only from user or managed settings. Keep in mind that the sandbox restricts shell commands, not the built-in Read tool, so you need deny rules as well. For checks you write yourself, a PreToolUse hook can inspect every command before it runs.
Keep real values out of the agent's context
Deny rules and the sandbox limit what the agent can reach. The other approach is to keep real values out of the files and shell the agent works in. Two patterns are common.
Opaque references in code (EnvSave)
EnvSave stores secrets in a local AES-256-GCM vault. Your code refers to each secret by an opaque es_ key, an HMAC-SHA256 hash of the secret's name that only resolves through your vault. You run the password-protected steps yourself: envsave set STRIPE_SECRET_KEY asks for the value with hidden input and prints the opaque key. The agent only ever writes get("es_..."). Here is part of the real envsave --help output:
EnvSave — Local Secret Vault for LLM-Safe Environments
Usage:
envsave init [--env <f>] [--ssh-key <f>] Initialize vault
envsave set <key> [value] Add/update a secret (omit value to be prompted — safe for !, $, spaces)
envsave get <opaque_key> Retrieve a value (no password needed)
envsave show <key> Show real value by key name (password required)
envsave reveal <keyword> [keyword2]... Show opaque key(s) for any name matching keyword (password required)
envsave search <keyword> Search keys matching keyword (no password)
envsave scan [dir] Scan project for env variable usage
In Node or TypeScript, following the EnvSave docs:
import { get, has } from "@envsave/cli";
import Stripe from "stripe";
const stripe = new Stripe(get("es_EXAMPLEa8d2e1c4b7f9")); // throws if missing
if (has("es_EXAMPLE3c1f9b7d2e4a")) { // optional secret
Sentry.init({ dsn: get("es_EXAMPLE3c1f9b7d2e4a") });
}
In Python:
from envsave import get
from openai import OpenAI
client = OpenAI(api_key=get("es_EXAMPLE7a3f2b1e9c8d"))
The value is resolved inside the running process, so it doesn't appear in your source files or dotenv file, and it doesn't end up in the transcript when Claude reads the code. An agent can't list the vault, and envsave get is rate-limited. To be clear about the limits: if the agent runs a program that prints the secret, that output still contains the real value. Opaque references remove the plaintext copies sitting in files. They don't make a running program safe from itself. Pair them with the deny rules above.
Run-time injection (1Password, Doppler, Infisical)
Secret managers usually inject values as environment variables of a child process. Your files hold only references or nothing at all:
# 1Password: the env file holds op:// references, not values
op run --env-file="./app.env" -- npm run dev
# Doppler: secrets from the project's config
doppler run -- npm run dev
# Infisical
infisical secrets-manager run -- npm run dev
Sources: 1Password op run, Doppler CLI, Infisical CLI reference (the current command is secrets-manager run).
| EnvSave | 1Password CLI | Doppler | Infisical | |
|---|---|---|---|---|
| What the repo contains | get("es_…") calls | op:// references in an env file | Nothing secret | Project config file |
| Where the value appears | Inside the process, on get() | Child process environment | Child process environment | Child process environment |
| Storage | Local encrypted vault | 1Password account | Doppler cloud | Infisical cloud or self-hosted |
| Best fit | Solo developers and agents working locally | Teams already on 1Password | Teams that want hosted config per environment | Teams that want open source or self-hosting |
With the injection tools, the secret is in the environment of the wrapped process. If Claude launches doppler run -- printenv, the values go into the transcript. Use the sandbox's envVars rules or keep the wrapper out of commands Claude can approve on its own.
A short checklist
- Pick one Anthropic credential on purpose and check it with
/status. - Don't keep
ANTHROPIC_API_KEYexported in your shell profile if you also have a subscription. - Set a workspace spend limit in the Console.
- Add
Readdeny rules for dotenv files and secret folders. - Turn on the sandbox and deny the environment variables Claude's commands don't need.
- Replace plaintext values in your repo with opaque references or run-time injection.
If you are packaging these rules for a team, see Claude Code skills vs agents vs AGENTS.md for where each piece of configuration lives, and Claude Code skills best practices for writing the skill that teaches agents your secret-handling rules.
Want your agents to stop seeing real keys? EnvSave is free for local use and installs with npm install -g @envsave/cli. Its Claude Code skill makes the agent write get("es_...") calls instead of reading your dotenv file.