Back to blog

Your Claude API key: Claude Code, the SDK, and keeping it out of .env

Vlad Vidican 9 min read

You get a Claude API key from the API keys page of the Claude Console. Anthropic's SDKs read it on their own from ANTHROPIC_API_KEY, and so does Claude Code, which will use that key instead of your Pro or Max subscription once you approve it. That explains a lot of "credit balance" and "organization disabled" errors. This post covers where the key goes, how to check which credential Claude Code is using, and how to move the key out of your shell profile and .env into an encrypted Valt0 vault, for your own code and for Claude Code.

Get the key

Sign in to the Claude Console and open the API keys page. Click Create key, name it, choose when it expires, and set Linked account to yourself. Anthropic calls that a personal key, for your own development. For anything shared, such as a production service or a CI pipeline, it recommends a service account key instead.

Pick a workspace for the key as well. A key scoped to one workspace works as it is. A key that isn't has to send an anthropic-workspace-id header with every request, or the API answers with a 400 error, and the examples in this post don't send one. The expiry can't be changed later: Anthropic suggests Never for a key kept in a secrets manager and rotated by hand, and with a date you make a new key when it runs out and store that one instead.

The Console shows the full key, which starts with sk-ant-, once. Copy it then. If you lose it, you can't see it again; you make a new one. If the Create key button is greyed out, your role might not allow it: ask an organization admin to change your role, or to create a service account key for what you are building.

Where the SDK looks for it

Anthropic's docs tell you to set the key as an environment variable, and both SDKs read ANTHROPIC_API_KEY when you don't pass a key yourself:

app.py
from anthropic import Anthropic

client = Anthropic()  # reads ANTHROPIC_API_KEY
app.mjs
import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic(); // reads ANTHROPIC_API_KEY

When Claude Code uses the key instead of your plan

Claude Code checks several credentials in a fixed order, and ANTHROPIC_API_KEY comes before the subscription login you made with /login. In an interactive session it asks you once whether to use the key and remembers the answer. With claude -p, it always uses the key when one is set. So a key you exported months ago for a script can quietly take over from your Pro or Max plan. Claude Code's error reference names three symptoms:

  • Credit balance is too low, while you pay for a plan: an approved key sends requests through that key and its Console balance instead.
  • This organization has been disabled: the key belongs to a Console organization that was disabled.
  • Invalid API key · Fix external API key: the key was revoked or mistyped. Claude Code's docs add that direnv, dotenv shell plugins and IDE terminals can load a stale key from a .env file in your project without you noticing.

Check which ANTHROPIC_ variables your shell has. This prints the names only, so the key doesn't end up on your screen:

Terminal
env | grep -E '^ANTHROPIC_' | cut -d= -f1

Inside Claude Code, /status shows the credential in use. To go back to your subscription, run unset ANTHROPIC_API_KEY, take the export line out of your shell profile, and start claude again. To keep the variable but stop Claude Code from using it, turn off Use custom API key in /config.

"Your organization has disabled Claude subscription access"

This one is the other way round. The full message is Your organization has disabled Claude subscription access for Claude Code · Use an Anthropic API key instead, or ask your admin to enable access. It is a setting on your organization's side that local settings, environment variables and flags can't override, and running /login again with the same account gets the same error. Either your admin turns access on, or you sign in with a Console API key. If you go the key route, the last part of this post shows how to hand it to Claude Code from a vault. People on personal Pro and Max plans, with no admin to ask, have reported the same message in Claude Code's GitHub issues; if that's you, Anthropic support is the place to ask.

Where the key ends up

Anthropic's example is an export line, which often ends up in ~/.zshrc or ~/.bashrc. Projects often put the key in .env instead. Either way it is plain text in a file:

~/.zshrc · before
export EDITOR=vim
export ANTHROPIC_API_KEY=sk-ant-...

Anthropic's support article on keys says never to put the key directly in your code or configuration files, to keep a local .env out of git, and for cloud deployments to prefer encrypted secret storage over dotenv files. If a key turns up in a public GitHub repository, GitHub tells Anthropic, and Anthropic deactivates it and emails you. A copy in a file can be read by whatever reads that file, from a backup to a coding agent that was asked to look around the project. A variable in your shell profile is handed to every program you start from that shell.

Before: ~/.zshrc holds EDITOR and ANTHROPIC_API_KEY in plain text. After: ~/.zshrc holds EDITOR and CLAUDE_VAULT_ID, and the key is in claude.valt0, encrypted, handed to your app and to Claude Code by the Valt0 service when they ask

Keep the key in a vault

A personal key is yours alone (Anthropic's advice is that each person gets their own), so give it a vault of its own, in a folder of your own rather than in a project. You need Valt0 installed; valt0 status checks that its service is running.

Terminal
mkdir -p ~/.claude-key
cd ~/.claude-key
valt0 init claude

That makes claude.valt0 and prints its key once. Put the vault key in your password manager now: without it nobody can open the vault, you included. Then store the Claude key. Valt0 asks for the vault key, then for the value: paste the sk-ant- key.

Terminal
valt0 store claude:ANTHROPIC_API_KEY

Your code and Claude Code never need the vault key. They name the vault by its public id and ask the Valt0 service running on your machine for one secret. Print the id:

Terminal
valt0 info claude

In your shell profile, replace the key line with the id. It is safe there:

~/.zshrc · after
export EDITOR=vim
export CLAUDE_VAULT_ID=your-vault-id

Open a new terminal afterwards, so the old key is no longer in the environment of what you run. If a project's .env also has the key, take that line out too.

Python

Valt0's client needs Python 3.9 or later and the current anthropic SDK needs 3.10, so use 3.10 or later:

Terminal
python3 -m pip install valt0-client anthropic

Pass the key to the SDK yourself, from the vault:

app.py · after
import os

import valt0
from anthropic import Anthropic

vault = valt0.open_vault(id=os.environ["CLAUDE_VAULT_ID"], name="claude")

with vault.secret("ANTHROPIC_API_KEY") as secret:
    client = Anthropic(api_key=secret.reveal())

message = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello, Claude"}],
)
print(message.content)

reveal() hands the SDK the value as a string, which the SDK keeps and Python can't wipe, and the with block wipes Valt0's own copy when it ends. Anywhere else, the secret prints as [REDACTED]: in print(), an f-string or a log line. If the service can't be reached, open_vault raises an error and the script stops before it calls Claude.

Node.js

Terminal
npm install @valt0/client @anthropic-ai/sdk
app.mjs · after
import Anthropic from '@anthropic-ai/sdk';
import { openVault } from '@valt0/client';

const vault = await openVault({ id: process.env.CLAUDE_VAULT_ID, name: 'claude' });

const apiKey = await vault.secret('ANTHROPIC_API_KEY');
const client = new Anthropic({ apiKey: apiKey.reveal() });
apiKey.destroy();

const message = await client.messages.create({
  model: 'claude-opus-5-5',
  max_tokens: 1024,
  messages: [{ role: 'user', content: 'Hello, Claude' }],
});
console.log(message.content);

destroy() wipes Valt0's copy straight after. The SDK keeps the string it was given, because a JavaScript string can't be wiped, but the key is no longer in process.env or in a file next to your code. Valt0's client and the current Anthropic SDK both need Node.js 20 or later.

Give Claude Code the key from the vault

Claude Code has a setting for this: apiKeyHelper, a command it runs to get the key, which must print the key and nothing else. Anthropic's docs suggest it for keys that come from a vault. A few lines of Python with Valt0's client do the job.

Give the helper its own small Python environment, so it works from any folder:

Terminal
python3 -m venv ~/.claude-key/venv
~/.claude-key/venv/bin/python -m pip install valt0-client

Save the helper next to the vault, with the id that valt0 info claude printed:

~/.claude-key/key.py
import sys

import valt0

vault = valt0.open_vault(id="your-vault-id", name="claude")

with vault.secret("ANTHROPIC_API_KEY") as secret:
    sys.stdout.write(secret.reveal())

Check that it works without putting the key on screen. This prints how many characters the helper wrote. If the service can't be reached, you get an error and a 0 instead:

Terminal
~/.claude-key/venv/bin/python ~/.claude-key/key.py | wc -c

Then point Claude Code at it: add this line to your user settings, keeping whatever else the file already holds. Use this file and not a project's .claude/settings.json, which is committed and shared with everyone who clones the repository:

~/.claude/settings.json
{
  "apiKeyHelper": "~/.claude-key/venv/bin/python ~/.claude-key/key.py"
}

ANTHROPIC_API_KEY ranks above the helper, so make sure it is gone from your shell, then start claude and check /status: it should name apiKeyHelper as the source. Claude Code keeps what the helper printed for five minutes, then runs it again. If the helper fails, for example because the Valt0 service isn't running, Claude Code shows Your apiKeyHelper script is failing; run the check above to see why.

The helper also ranks above your /login, so set it up only when you want Claude Code to bill this key. While it is set, Remote Control and voice dictation, which need your claude.ai login, aren't available. To go back to your subscription, remove the apiKeyHelper line. The setting is read by the claude CLI and the tools that run it, such as the VS Code extension and the Agent SDK. The Claude Desktop app and cloud sessions don't call it and sign in with your account, unless the desktop app is set up for a third-party gateway.

From here

  • If the key may have leaked, delete it on the API keys page straight away, which is Anthropic's advice. Then create a new key, store it from ~/.claude-key with valt0 store claude:ANTHROPIC_API_KEY --force, and restart what uses it.
  • Teammates make their own personal key and their own vault the same way. Anthropic's advice is that everyone who needs access gets their own key.
  • Keep it out of browser code. The TypeScript SDK turns browser support off by default to avoid exposing your key, and only runs there if you set dangerouslyAllowBrowser. Call Claude from your server.
  • Production gets a service account key in a vault of its own, committed with the app, under the same secret name. Deployment covers servers, and the clients are in the Python and Node.js docs.

Other secrets in the same .env go the same way: Turn an .env file into a vault converts a whole file in one step, and the Gemini and OpenAI versions of this post cover those keys.

Sources: Anthropic: Get your Claude API key, Anthropic: Select a workspace, Anthropic: API key best practices, Claude Code: Authentication, Claude Code: Errors, Claude Code: apiKeyHelper, anthropic for Python, @anthropic-ai/sdk for TypeScript, Valt0 Python client docs, Valt0 Node.js client docs.

Create your first vault.

$ valt0 init prod

Install