◉CLAUDE LABJP
●2.1.302 — Fork subagents are now on by default in claude -p, the Agent SDK and the desktop app. Turn them off with CLAUDE_CODE_FORK_SUBAGENT=0 (10/16)●2.1.307 — Fixes MCP failing to reconnect after sleep or a keychain lock (10/21). 2.1.306 also fixed the false claude -p login message●SONNET 4.5 — Retires on the Claude API on 11/30, 38 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.302 — Fork subagents are now on by default in claude -p, the Agent SDK and the desktop app. Turn them off with CLAUDE_CODE_FORK_SUBAGENT=0 (10/16)●2.1.307 — Fixes MCP failing to reconnect after sleep or a keychain lock (10/21). 2.1.306 also fixed the false claude -p login message●SONNET 4.5 — Retires on the Claude API on 11/30, 38 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-10-22Beginner

When Only claude -p Refuses to Log In: Check the Credential Order Before You Re-Login

Interactive Claude Code works but claude -p or a script is refused? Here is where to look before running /login again, following the credential order in the official docs.

Claude Code271authentication7claude -ptroubleshooting98environment variables5CLAUDE_CODE_OAUTH_TOKEN

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.

RankCredentialWatch out for
1Bedrock / Vertex / Foundry variablesA leftover CLAUDE_CODE_USE_BEDROCK hides everything below it
2ANTHROPIC_AUTH_TOKENA bearer token meant for gateways
3ANTHROPIC_API_KEYIn -p mode it is always used when present
4apiKeyHelper outputIf the script fails, requests fail after a few attempts
5CLAUDE_CODE_OAUTH_TOKENThe long-lived token from claude setup-token
6Anthropic profileNaming one with ANTHROPIC_PROFILE puts it above /login
7Subscription login from /loginThe 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
done

If 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-token

A 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:

  1. Look at /status to see the method actually in use.
  2. Run the script above to list the variables that affect priority.
  3. For -p or unattended runs, decide between an API key and a long-lived token.
  4. Remember where credentials live for this environment (terminal, SSH, cloud).
  5. Run /login again 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.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⟐ Claude Code2026-09-20
I Was Paying for Pro and Still Getting Billed by the Console
Your subscription and your API usage are two separate ledgers, and Claude Code will happily run on either one. Which credential wins is decided by a precedence list where your login sits dead last. Here is how to read status, and why unattended runs answer differently.
⟐ Claude Code2026-10-20
You Stepped Away and Came Back to a Summary — Spotting Idle Compaction and Keeping the Details You Can't Lose
Since v2.1.299, Claude Code compacts a long conversation after about 54 idle minutes. Here is how to tell it happened, how to tune or switch it off, and how to move the assumptions you can't afford to lose into CLAUDE.md.
⟐ Claude Code2026-10-19
If You Wrote MCP Allow Rules as Just `mcp__servername` — Three Lines to Recheck After v2.1.299
Claude Code v2.1.299 changed MCP allow rules to match only the server they name. Here is how to audit older entries in settings.json, with a short Python script that does it.