You run /login again. The terminal says "Login successful." And yet claude -p, called from inside a script, refuses you in exactly the same way.
It's the kind of moment where your hands stop, because it looks as if login itself is broken. You re-login twice, three times, and the only thing that grows is the number of browser tabs. I'd like to spare you that detour.
Let me say the main point first: in many cases, login isn't what's broken. The question is which credential has been chosen. The official authentication docs spell out the order in which Claude Code picks one. Here I'm turning that order into steps you can check by hand.
Start with /status: which method is actually in use?
Before re-logging in, open /status in an interactive session and look at the login-method row.
According to the docs, when both a login and an API key are configured, the one that isn't in use is marked. If a profile is selected, a Profile row appears in place of the login-method row. And when a saved claude.ai login is the active credential and has expired, a Login row reads "Expired — log in again" (v2.1.210 or later).
So /status tells you whose name you're running under right now. Typing /login without looking at it is a bit like re-cutting a key without checking the lock.
Copy the priority table into your notes
When several credentials exist, Claude Code picks one in this order. Higher wins.
| Rank | Credential | Watch out for |
|---|---|---|
| 1 | Bedrock / Vertex / Foundry variables | A leftover CLAUDE_CODE_USE_BEDROCK hides everything below it |
| 2 | ANTHROPIC_AUTH_TOKEN | A bearer token meant for gateways |
| 3 | ANTHROPIC_API_KEY | In -p mode it is always used when present |
| 4 | apiKeyHelper output | If the script fails, requests fail after a few attempts |
| 5 | CLAUDE_CODE_OAUTH_TOKEN | The long-lived token from claude setup-token |
| 6 | Anthropic profile | Naming one with ANTHROPIC_PROFILE puts it above /login |
| 7 | Subscription login from /login | The default for Pro / Max / Team / Enterprise |
/login sits at number seven. If any of the six above it is lingering, fixing only /login changes nothing.
The usual reason only -p fails: an API key in the environment
In interactive mode, when ANTHROPIC_API_KEY is found, you're asked once whether to use it, and your answer is remembered. In -p (non-interactive) mode, the key is always used if it's there. Nothing asks you.
Picture a subscription account with an old API key still in a shell profile. Interactive sessions work because of the "don't use it" answer you gave once, while -p goes straight to a key from an organization that no longer works. The docs say as much: a key from a disabled or expired organization causes authentication failures.
The safe way to check is to look at whether variables are set, not at what's in them.
# What this solves: list which credential-related variables exist in the current shell
# (values are never printed; this syntax is bash-only)
for v in CLAUDE_CODE_USE_BEDROCK CLAUDE_CODE_USE_VERTEX CLAUDE_CODE_USE_FOUNDRY \
ANTHROPIC_AUTH_TOKEN ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN \
ANTHROPIC_PROFILE CLAUDE_CONFIG_DIR; do
if [ -n "${!v}" ]; then echo "$v: set"; else echo "$v: -"; fi
doneIf ANTHROPIC_API_KEY: set shows up and you don't recognize it, try unset ANTHROPIC_API_KEY, then confirm with /status that you're back on your subscription.
Why not print the values? Because screen shares and pasted logs have a way of keeping a key long after you meant to hide it.
For unattended runs, don't bring browser login along
Scheduled jobs and scripts can't approve a browser login. A long-lived token is the mechanism for that.
# What this solves: hand subscription auth to an environment with no interactive login
claude setup-token
# The token is printed after you approve. It is not saved, so copy it and set it:
export CLAUDE_CODE_OAUTH_TOKEN=your-tokenA few traits of this token are worth knowing. It lasts one year and needs a Pro, Max, Team, or Enterprise plan. It can only make model requests, so Remote Control sessions and claude.ai connectors aren't available. And a run with --bare doesn't read this variable; in that case use an API key or an apiKeyHelper.
If you're logged in but connectors don't show up, suspect the token's limits first. That's usually the shortest path.
Where it runs changes where credentials live
The same claude reads different things depending on where it runs.
On macOS, credentials go into the Keychain. If the Keychain refuses the write, for example in an SSH session, they're stored in ~/.claude/.credentials.json instead. With CLAUDE_CONFIG_DIR set, the file under that directory is used, and the Keychain entry is keyed per directory as well.
So your terminal and a script running under another config directory may be looking at two different logins.
One more: cloud sessions and Claude Desktop don't read apiKeyHelper or ANTHROPIC_API_KEY; they use OAuth. Putting a key in a cloud environment doesn't override your subscription credentials. If claude -p in the cloud shows a login message that doesn't add up, check the release changelog for a fix as well. In a summary I came across, a recent version mentioned correcting that kind of misleading message. Please confirm the details in the changelog itself.
Expiry is the one case where re-login is the answer
If you've checked the whole order and /status shows "Expired — log in again," then logging in again is correct.
A saved login warns at startup from three days before it expires. The warning doesn't block requests, but once it expires and can't be refreshed, every request fails with "Login expired · Please run /login." A long-running unattended session stops making progress at that point and can't recover by itself.
The order I settle on
Put together, it becomes this:
- Look at
/statusto see the method actually in use. - Run the script above to list the variables that affect priority.
- For
-por unattended runs, decide between an API key and a long-lived token. - Remember where credentials live for this environment (terminal, SSH, cloud).
- Run
/loginagain only when the login has expired.
Before fixing the login, say out loud which credential is winning. I've settled on this habit as my own rule, and I'd expect it to cut your re-logins too.
Today, on the machine you actually use, open /status and run the variable check once. Write the names you see in one line of your notes; the next time the same message appears, your hands won't have to stop.