Skip to content

Missing API key · Run /login

Nothing was sent. No request left your machine, no server rejected anything, and no credential was found to be bad — Claude Code looked through the places it knows about, came up empty, and stopped before making a call. That rules out an expired key, a revoked key, a wrong organization, a rate limit, and every vendor-side fault, because all of those require a request that actually happened.

This is a resolution failure, and the useful question is not “is my key valid” but “which process, running as which user, in which environment, is doing the looking”.

Why this is not the same error as an invalid key

The distinction sounds pedantic until it changes what you do. When a credential is present and refused, the fix lives at the credential: replace it, unset the one that is shadowing it, point at a different endpoint. When nothing resolves at all, the credential is usually fine and sitting exactly where you left it — the process asking for it just cannot see it.

So the entire diagnostic set is different. Regenerating a key cannot fix this error, because there is no key in play to invalidate. Neither can switching organizations, raising a limit, or waiting. The failing thing is the lookup.

Invalid API key · Please run /login is the other side of that line, and its central case — a stray ANTHROPIC_API_KEY in your environment quietly winning over your subscription — is structurally impossible here. An environment variable that is set is the opposite of a credential that is missing.

The advice in the message is not always followable

/login is a slash command. Slash commands exist inside an interactive session, and a large share of the situations that produce this error have no interactive session to type it into: a claude -p run, a CI job, an Agent SDK application, a hook, a scheduled task, a container entrypoint, a service running as a different account.

In those contexts the error prints an instruction you cannot follow, which is why it reads as a dead end. The fix is never to find a way to run /login there; it is to supply the credential through a channel that context can actually consume, or to make that context inherit the environment where the credential already lives.

A related asymmetry is worth knowing: the docs note that /usage-credits is not available with API key authentication. Claude Code’s auth modes are not interchangeable skins over the same thing — they differ in what the session can do — so “just use whichever one works” is not free advice.

Telling the causes apart

Each of these has an observation that confirms or kills it. Run them in order; the first one that matches is yours.

An interactive claude in the same directory works, but your script does not. The environment differs, not the credential. Compare env output between the two contexts rather than guessing — a login shell reads profile files that a CI runner, a cron entry, and a GUI-launched process do not.

It fails under sudo, in a container, as a service account, or on a build agent. You are a different user, so ~ is a different home. Claude Code keeps global app state and OAuth in ~/.claude.json, and on Windows ~ means %USERPROFILE%. A credential written by your desktop account is not visible to a service account, and no amount of re-logging-in as yourself will change that.

It started after someone set CLAUDE_CONFIG_DIR. That variable relocates Claude Code’s configuration. Set in one shell and not another, or pointed at a fresh empty directory, it produces a perfectly working install that has never seen a credential. Confirm the variable has the same value in the shell where you logged in and the shell that fails.

An administrator was supposed to provision it and says they did. Managed settings live in a system directory that differs per OS: macOS /Library/Application Support/ClaudeCode/managed-settings.json, Linux and WSL /etc/claude-code/managed-settings.json, Windows C:\Program Files\ClaudeCode\managed-settings.json. The legacy Windows path C:\ProgramData\ClaudeCode\managed-settings.json is not read at all — a file placed there is silently ignored, which looks from the user’s side exactly like a missing credential. This one is worth checking early on any managed Windows fleet, because the admin’s evidence and the user’s evidence both look correct.

A credential helper is configured and returns nothing. If your setup shells out to a helper for the credential, a helper that exits non-zero or prints an empty string resolves to no credential. Run it by hand as the failing user and look at what it actually produces.

A credential resolves after you fix the above, and the endpoint then refuses it. That is progress, not a regression — you have moved to a different error. A 401 saying OAuth is not supported means the context now has a credential and needs a different kind of one, which is common when a script targets a gateway or a cloud deployment.

It appeared after you installed a plugin, an MCP server, or a hook. Start with claude --safe-mode, which launches with all plugins, MCP servers, and hooks disabled. If the error disappears, a customization is mutating the environment; if it persists, stop suspecting them.

Is retrying useful?

No — and unlike a flaky network, there is not even a small chance it helps.

A lookup that found nothing will find nothing again a second later. There is no backoff, no capacity window, and no retry-after to honor, because the server was never involved in the decision. If a wrapper around your script retries this error, it is burning wall-clock time on a verdict that was already final and hiding the real message under a pile of identical ones.

Re-running is only informative as a controlled experiment: run once as yourself in an interactive terminal, and once in the failing context, and compare. The difference between those two runs is the answer.

Fix by scenario

  • Non-interactive run, credential exists interactively — pass the credential explicitly into that context rather than relying on inheritance. CI runners, containers and service managers start from a near-empty environment by design.
  • Wrong user or wrong home — log in as the account that will actually run Claude Code, or provision the credential for that account. Verify from that account, not from yours.
  • CLAUDE_CONFIG_DIR mismatch — make it consistent, then log in once under the value you intend to keep.
  • Managed settings on Windows — move the file to C:\Program Files\ClaudeCode\managed-settings.json. The legacy location does not work no matter how correct its contents are.
  • You cannot get a session at all — run claude doctor from the shell. It is a read-only installation and settings diagnostic that does not need a session to start, which is the specific reason it exists.

A standing rule while you do any of this: do not paste a key into a chat, a web form, or a third-party validator to “check” it, and do not print its value into a terminal or a CI log. Test for presence, not content — env | grep -c ANTHROPIC_API_KEY tells you what you need without revealing anything, and CI logs are archived far longer than people expect.

How to confirm it’s fixed

Run the failing context, not a convenient one. If the error came from a CI job, the confirmation is a CI job; if it came from a service account, su to that account first. Inside a session, /status should name the credential source you provisioned — a working session that resolves a different credential than you intended means you fixed a symptom and left the real configuration in place.

Then run it twice, with a fresh process each time. A single success can come from a shell that still holds an environment you exported by hand, and that environment dies with the window.

If a credential resolves and gets refused, you are on the other page: Invalid API key · Please run /login. If the sign-in itself never completes, the failure is in the exchange rather than the lookup — a login that dies with fetch failed never reached a server at all, which is a network story wearing an auth label. To place your own symptom on that map — sent and refused, never sent, or sent and never answered — use the AI coding error triage tool.