Fix Claude Code's invalid API key error
Claude Code says invalid API key while you pay for a subscription? Find the stray ANTHROPIC_API_KEY on your VPS, and learn why /login will not fix it.
Why Claude Code shows an invalid API key error
Claude Code fails with Invalid API key for two different reasons, and the two fixes are opposites. Either you meant to authenticate with an API key and that key is wrong, revoked, or from another account. Or you never meant to use a key at all, and a leftover ANTHROPIC_API_KEY in the server's environment is outranking the subscription you logged in with. Anthropic's documentation, as of September 2026, is direct about the second case: a key set in the environment is used instead of your Claude Pro, Max, Team, or Enterprise subscription even when you are logged in.
Find out which branch you are on before you change anything. Start Claude Code and run /status. The documentation describes a Login method row that shows your subscription account, and an API key row that appears when an API key is in use. If you see the API key row on a box where you only ever ran /login, the environment is your problem, and re-authenticating will not touch it.
Everything below is a command for you to run on your own server. Read the output before you act on it.
The credential order, and why /login does not help
When several credentials are present, Claude Code picks one in a documented order:
- Cloud provider credentials, when
CLAUDE_CODE_USE_BEDROCK,CLAUDE_CODE_USE_VERTEX, orCLAUDE_CODE_USE_FOUNDRYis set. - The
ANTHROPIC_AUTH_TOKENvariable, sent as anAuthorization: Bearerheader. - The
ANTHROPIC_API_KEYvariable, sent as theX-Api-Keyheader. - The output of an
apiKeyHelperscript. - The
CLAUDE_CODE_OAUTH_TOKENvariable, which holds a token fromclaude setup-token. - Anthropic profile and federation credentials.
- The subscription OAuth credentials that
/loginwrites.
Read that list from the bottom. /login writes the credential in last place, which on Linux lands in ~/.claude/.credentials.json with file mode 0600. Every environment credential above it wins. So a fresh login refreshes a credential the session never reaches for: the login worked, and it lost. That is the whole reason the obvious fix does nothing.
Interactive sessions add one more step that confuses people. The documentation says you are prompted once to approve or decline an API key found in the environment, and your choice is remembered. The approval you clicked through months ago is still in force. You change it with the "Use custom API key" toggle in /config, and that toggle only appears while ANTHROPIC_API_KEY is set in your environment. In non-interactive mode, meaning claude -p inside a script or a cron job, there is no prompt at all: the key is always used when present.
How do I find the stray ANTHROPIC_API_KEY on a VPS?
First, confirm it exists in the shell you start Claude Code from.
env | grep -i anthropicThen run the fix Anthropic's own troubleshooting page gives, which doubles as the test:
unset ANTHROPIC_API_KEY
claudeIf Claude Code starts and /status now names your subscription, you have proved the cause. The variable returns in the next shell, because unset only changes the shell you typed it in. The rest of this section is about finding whatever sets it.
Shell profiles and system-wide environment files
grep -rn ANTHROPIC ~/.bashrc ~/.bash_profile ~/.profile ~/.zshrc \
~/.config/fish/config.fish /etc/environment /etc/profile /etc/profile.d/ 2>/dev/nullAnthropic's page names ~/.zshrc, ~/.bashrc, and ~/.profile. On a server, widen the search. /etc/environment is read by PAM (pluggable authentication modules) at login for every user on the box, which is how a key set by a colleague ends up in your session. Files in /etc/profile.d/ run for login shells. Note that .bashrc is read only by interactive shells, so it can never explain a failure inside a systemd service. The file that matters depends on how Claude Code was started.
systemd units
A unit does not read your shell profile. Its environment comes from Environment= and EnvironmentFile= lines in the unit and in any drop-in.
systemctl cat claude-agent.service
systemctl show -p Environment claude-agent.servicesystemctl cat prints the unit file followed by every drop-in from /etc/systemd/system/claude-agent.service.d/, which is where an override usually hides. systemctl show -p Environment prints what systemd will actually pass to the process. For a service running under your own user, add --user to both commands. After you edit a unit, run sudo systemctl daemon-reload and restart the service, because the environment is assembled when the process starts and a running process keeps the copy it was handed.
tmux and screen sessions that outlived your edit
This one costs people hours. A tmux server keeps the environment it was started with, and new panes inherit from the server rather than from your current shell. You remove the export from .bashrc, open a new pane, and the old key is still there.
tmux show-environment | grep -i ANTHROPIC
tmux set-environment -r ANTHROPIC_API_KEYset-environment -r marks the name for removal from the environment tmux hands to new processes, and adding -g does the same for the server's global environment. Panes that are already open keep their own copy, because a process's environment can only be changed from inside that process. The reliable move after removing the export is to detach, run tmux kill-server, and start a new session. screen behaves the same way. Worth knowing before you set up a long-running Claude Code session in tmux on a VPS, since that session is exactly the kind of process that outlives three rounds of config edits.
To read the environment of a process that is already running, ask the kernel for it:
tr '\0' '\n' < /proc/$(pgrep -n claude)/environ | grep -i anthropicThat prints the values the process was handed at exec time, which is what it is really using. You need to be the process owner or root to read that file, and pgrep -n claude picks the newest match, so check the PID if several are running.
Docker and Compose
docker exec claude-agent env | grep -i anthropic
docker compose configThe first command shows the environment inside a running container, including anything from --env-file, an environment: block, or an ENV line baked into the image. docker compose config prints the compose file with variables resolved, so you see the value that will be passed rather than the ${ANTHROPIC_API_KEY} placeholder you wrote. Both print secrets to your terminal, so run them in a session you are willing to clear. Compose also loads a .env file sitting next to the compose file without being told to, and that is the usual source of a key nobody remembers adding.
The settings file that survives every shell fix
Claude Code settings files carry an env block, and the documentation is explicit about the conflict: when the same variable is set in both the shell and a settings file env block, the settings file value applies. A key written there beats every unset you type.
grep -rn ANTHROPIC ~/.claude/settings.json .claude/settings.json .claude/settings.local.json 2>/dev/nullCheck the project files as well as the user file. .claude/settings.json is normally committed, so it is shared with everyone who clones the repository. Organizations can also push managed settings, which outrank your own files. If you find a key you cannot remove, that is who to ask.
The other branch: the key really is wrong
If /status shows an API key and you meant it to, take the error at face value. Anthropic's error reference gives these causes for Invalid API key:
- The key is malformed or incorrect.
- The key has been revoked or has expired.
- The key belongs to a different organization or account.
Inspect the value without printing it into your scrollback:
echo "len=${#ANTHROPIC_API_KEY} tail=${ANTHROPIC_API_KEY: -6}"A length one or two characters longer than you expect usually means a trailing newline or a quote came along with the copy and paste, or with a $(cat keyfile) that captured the newline at the end of the file. The value goes out as the X-Api-Key header, so a stray character means the credential sent is not the key you created.
Check ANTHROPIC_AUTH_TOKEN in the same output, because it sits above the API key in the order. A leftover bearer token from a proxy experiment means the key you are busy fixing is not the credential being sent at all. ANTHROPIC_BASE_URL is worth reading in that output too, since an old value points the client at a gateway that may no longer exist. For the header-level picture, how Anthropic API key authentication works covers what each of those carries. Before you decide which credential this machine should hold at all, the trade between a subscription login and an API key is the background reading.
One thing this error is not: a capacity problem. If the session authenticates and then requests fail while it is working, you are looking at the overloaded model error, which needs no credential change.
Why does my apiKeyHelper script fail?
apiKeyHelper is a settings key naming a script that Claude Code runs to get a credential. It exists for rotating or short-lived tokens, such as a key fetched from a vault. The contract is small: print the current key on standard output and exit successfully. The documentation says a script that exits with an error, times out, or prints nothing makes requests fail with Your apiKeyHelper script is failing within three attempts.
Run it by hand and check both halves of that contract:
out=$(/usr/local/bin/anthropic-key.sh)
echo "exit=$? len=${#out} tail=${out: -6}"A non-zero exit is a failure even when the key printed correctly. A helper that prints Fetching credential... to standard output before the key is also a failure, because that line becomes part of the credential. Send progress messages to standard error instead.
Then test it the way a service will run it:
env -i HOME="$HOME" PATH=/usr/bin:/bin /usr/local/bin/anthropic-key.sh
echo "exit=$?"env -i starts the script with an almost empty environment. A helper that calls aws, vault, or gcloud from a directory your .bashrc adds to PATH works when you test it and fails in place, because the running Claude Code process never read your .bashrc. Make sure the script is executable and that everything it calls uses an absolute path, or set PATH inside the script itself.
Two more documented behaviours matter on a server. Claude Code re-runs the helper after five minutes by default, and CLAUDE_CODE_API_KEY_HELPER_TTL_MS sets a different interval, so a setup that works at startup and breaks an hour later is failing on a refresh rather than at launch. If the helper takes more than ten seconds to return a key, Claude Code shows a warning notice in the prompt bar with the elapsed time. That notice means a slow call, not a broken one, and it is your early warning before the timeout turns into an error.
Do you actually want the API key on this box?
The fix is not always removal. Keep the key when the machine has a reason to bill separately:
- An unattended agent on a VPS billed to Console does not spend a person's subscription limits.
- Non-interactive runs, where
claude -phas no terminal to approve anything and the key is always used when present. - Machines with no subscription attached to them.
- A shared or client machine where a personal subscription login should not be stored at all.
Cost usually decides this, and the API and subscription pricing comparison is where to work it out.
Remove the key when the box is yours and the subscription is what you already pay for. Then make the removal stick. Instead of exporting the key in ~/.bashrc, where every interactive shell inherits it, give it only to the service that needs it:
[Service]
EnvironmentFile=/etc/claude-agent.envKeep that file at mode 600, owned by the user the service runs as. Your interactive sessions never see it, so your own claude keeps using the subscription while the service keeps using the key. If you need a subscription credential somewhere with no browser, claude setup-token prints an OAuth token to paste into CLAUDE_CODE_OAUTH_TOKEN. Its documented limits are worth knowing before you rely on it: it can only make model requests, so Remote Control sessions and claude.ai connectors are not available with it. Deciding this once per machine, and writing it in the unit file, is what stops the failure this page is about, because it starts with a key nobody remembers setting. Limiting what that credential can reach once it is in place is a separate job, covered in running Claude Code safely on a VPS.
One caution before you reach for the big hammer. /logout removes stored credentials, and the documentation notes that it also clears saved MCP (model context protocol) server logins and plugin secrets, so expect to re-authorize those afterwards.
FAQ
Why does Claude Code say invalid API key when I pay for a subscription?
Because an ANTHROPIC_API_KEY in the environment outranks the subscription login. Anthropic's documentation says a key set in the environment is used instead of your Pro, Max, Team, or Enterprise subscription even when you are logged in, and that in non-interactive mode with -p the key is always used when present. Run /status inside Claude Code to see which credential the session picked. If an API key row appears and you never set one on purpose, run unset ANTHROPIC_API_KEY and start claude again to confirm the cause.
Does running /login fix an invalid API key error?
Not while the variable is set. /login writes subscription OAuth credentials, and those sit at the bottom of Claude Code's credential order, below the environment variables and below apiKeyHelper. The login succeeds and is then passed over, which is why repeating it changes nothing. Remove the variable from wherever it is set, or turn off the "Use custom API key" toggle in /config, which the documentation says appears only while ANTHROPIC_API_KEY is set in your environment.
How do I see which authentication method Claude Code is using?
Run /status in the session. The documentation describes a Login method row showing your subscription account, and an API key row that appears when an API key is in use. Compare that against env | grep -i anthropic in the same shell you launched from. If a service or a container runs Claude Code, read the process environment instead with tr '\0' '\n' < /proc/<pid>/environ, because a running process keeps the environment it was given when it started, not the one your current shell has.
My apiKeyHelper script works when I run it. Why does Claude Code still fail?
Usually the environment or the exit status. Claude Code runs the helper from its own process, which never read your shell profile, so a helper depending on a PATH entry from .bashrc fails in place while working in your terminal. Test it with env -i HOME="$HOME" PATH=/usr/bin:/bin /path/to/helper and check echo $? afterwards. The documented failure covers a script that exits with an error, times out, or prints nothing, and it surfaces as Your apiKeyHelper script is failing. Anything printed to standard output other than the key becomes part of the credential, so send progress messages to standard error.