AIagentsLab
Back to Blog
Claude CodeAPI KeysSecurityEnvSave

Claude Code API Key: Setup, Billing and Secret Safety

Jose Carlos SanchoNovember 11, 202610 min read

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:

MethodHow you set itWho paysGood for
claude.ai subscription/login, choose your claude.ai accountPro, Max, Ultra, Team or Enterprise planDaily interactive use
Claude Console login/login, choose the Anthropic Console accountConsole credits (API pricing)Teams that want API billing without handling raw keys
ANTHROPIC_API_KEYEnvironment variableConsole creditsScripts, claude -p, the Agent SDK
apiKeyHelperA command in settings.json that prints a keyWhoever owns the keyKeys from a vault or short-lived tokens
CLAUDE_CODE_OAUTH_TOKENToken from claude setup-tokenYour subscriptionCI 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

  1. 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.
  2. 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
  3. 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.
  4. 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:

  1. Cloud provider credentials, when CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX or CLAUDE_CODE_USE_FOUNDRY is set
  2. ANTHROPIC_AUTH_TOKEN (bearer token, for LLM gateways and proxies)
  3. ANTHROPIC_API_KEY (sent as X-Api-Key)
  4. apiKeyHelper output
  5. CLAUDE_CODE_OAUTH_TOKEN
  6. Anthropic profile and federation credentials
  7. 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. /usage shows 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

MessageUsual causeFix
Credit balance is too lowConsole credits ran out, or an approved key is overriding your subscriptionCheck the API key row in /status. Unset the variable or add credits
Invalid API key · Fix external API keyTypo, revoked key, or a stale key loaded by direnv, a dotenv shell plugin or your IDE terminalRun env | grep ANTHROPIC in the same shell, then check the key in the Console
Your apiKeyHelper script is failingThe helper exited with an error, printed nothing, or printed more than the keyRun 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 (set cleanupPeriodDays to 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).

EnvSave1Password CLIDopplerInfisical
What the repo containsget("es_…") callsop:// references in an env fileNothing secretProject config file
Where the value appearsInside the process, on get()Child process environmentChild process environmentChild process environment
StorageLocal encrypted vault1Password accountDoppler cloudInfisical cloud or self-hosted
Best fitSolo developers and agents working locallyTeams already on 1PasswordTeams that want hosted config per environmentTeams 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

  1. Pick one Anthropic credential on purpose and check it with /status.
  2. Don't keep ANTHROPIC_API_KEY exported in your shell profile if you also have a subscription.
  3. Set a workspace spend limit in the Console.
  4. Add Read deny rules for dotenv files and secret folders.
  5. Turn on the sandbox and deny the environment variables Claude's commands don't need.
  6. 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.

Need help implementing AI agents?

We design, build and run custom AI agents for businesses, using the same open tools we publish. Let's discuss your project.