The error, verbatim
Jump to the fix ↓Invalid API key · Fix external API key
# the other messages for the same problem:
Not logged in · Please run /login
Failed to authenticate. API Error: 401 API key is invalid.
Failed to authenticate. API Error: 401 OAuth access token has expired. Re-authenticate to continue.
Tested on
- Claude Code
- 2.1.138
- OS
- Windows 11 Pro 26200
- Shells
- cmd.exe, PowerShell 5.1, Git Bash
- Keys
- fake keys only, in a separate config folder
Contents
Claude Code can sign in two ways: your Claude account (/login), or an API key from the ANTHROPIC_API_KEY environment variable. Every message above means one of those two is missing, malformed or dead. The message tells you which, if you know how to read it.
Which message do you have?
| Message | What it means | Go to |
|---|---|---|
Invalid API key · Fix external API key |
The key in ANTHROPIC_API_KEY isn’t even shaped like a key |
1 |
Nothing for minutes, then 401 API key is invalid |
The key looks fine, but Anthropic rejects it | 2 |
401 OAuth access token has expired |
Your /login session ran out |
3 |
Not logged in · Please run /login |
No login and no key at all | 3 |
1. “Invalid API key · Fix external API key”: quotes in the key
This one appears instantly, before any request is made. Claude Code checks the key’s format first, and the usual reason it fails on Windows is quotation marks that became part of the key.
In cmd.exe, quotes around the value are kept as characters:
C:\> set ANTHROPIC_API_KEY="sk-ant-api03-..."
C:\> claude -p hi
Invalid API key · Fix external API key
Put the quotes around the whole assignment instead, or none at all:
set "ANTHROPIC_API_KEY=sk-ant-api03-..."Single quotes do the same damage ('sk-ant-...' failed identically). PowerShell’s $env:ANTHROPIC_API_KEY = "sk-ant-..." is fine: there the quotes are syntax, not part of the value.
2. A frozen terminal, then “401 API key is invalid”
A key with the right shape but the wrong value (revoked, deleted, a typo in the middle) doesn’t fail fast. I ran claude -p with one:
C:\> claude -p "say hi"
← nothing for 191 seconds
Failed to authenticate. API Error: 401 API key is invalid.
The debug log shows why. Claude Code retries the rejected request up to 11 times, backing off longer each time, and prints nothing while it does:
[ERROR] API error (attempt 1/11): 401 {"type":"error","error":{"type":"authentication_error","message":"API key is invalid."}}
[ERROR] API error (attempt 2/11): 401 ...
[ERROR] API error (attempt 3/11): 401 ...
So if Claude Code seems to hang on the first message, don’t assume a network problem. Run it once with a debug file and look:
claude -p "hi" --debug-file debug.txtThen search debug.txt for 401. If it’s there, the key is the problem: create a new one in the Anthropic Console and replace the old value everywhere it’s set (section 4).
3. “Please run /login” or an expired OAuth token
With no key and no login, the message is immediate:
Not logged in · Please run /login
An expired login is louder. I hit this one for real: the standalone CLI on the test machine hadn’t been used in a while, the token refresh failed, and every request came back with:
Failed to authenticate. API Error: 401 OAuth access token has expired. Re-authenticate to continue.
Sign in again. Inside Claude Code type /login; from a terminal:
claude auth loginThen confirm what it’s using:
claude auth status4. Does an old ANTHROPIC_API_KEY break a working login?
This is the most repeated advice for these errors: “unset ANTHROPIC_API_KEY”. I tested it with a working Claude account login and a dead key in the environment, in print mode (claude -p):
"authMethod": "claude.ai",
"apiKeySource": "ANTHROPIC_API_KEY",
The request succeeded. The login was used and the stale key was ignored, even when the key was the quoted, malformed kind from section 1.
Interactive mode is different. When it starts and finds a key in your environment, it shows “Detected a custom API key in your environment” and asks whether to use it. That prompt can’t be driven from a script, so I didn’t test it, but the logic follows: if you once said yes to a key that has since died, remove the key rather than fight the prompt.
Find where the key is set on Windows. A key saved to your user environment survives every restart:
[Environment]::GetEnvironmentVariable("ANTHROPIC_API_KEY","User")If that prints an old key, remove it:
[Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", $null, "User")Then close and reopen the terminal, or VS Code. I checked this both ways with a test variable: saving a value didn’t reach the window that was already open, and removing it didn’t either. Each window keeps the copy it started with.
What didn’t work
How this was tested
Claude Code 2.1.138 on Windows 11, run with claude -p and claude auth status. Malformed and invalid keys were fake values, run against a separate config folder (CLAUDE_CONFIG_DIR) so no real login was involved; quoting was tested in cmd.exe batch files, PowerShell and Git Bash. The expired-token case was real, on this machine’s own CLI, and was fixed by claude auth login. The stale-key-versus-login test used that fresh login plus a fake key in the environment. Retry counts and timings come from --debug-file logs.
— N.K., end of entry No.037