I was reading through a release diff published on September 8th when one line stopped me. Setups that run Claude Code behind a gateway or proxy had been failing every single request on the previous version.
The message they saw was Not signed in to the Cloud gateway. Nothing in the configuration had changed. It simply stopped working one morning.
If that sounds familiar, you may have settings.json open right now. That file is probably fine.
The short answer — a 2.1.265 regression, fixed in 2.1.266
The cause was a change in how CLAUDE_CODE_USE_GATEWAY is handled. It is an undocumented environment variable, which is part of why this was hard to see.
Before 2.1.265, the variable only meant something when both ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN were set. On its own it was ignored. In 2.1.265 it started forcing Cloud gateway sign-in all by itself.
So any setup that paired it with an API key, an apiKeyHelper, or custom auth headers began failing every request with Not signed in to the Cloud gateway.
Version 2.1.266, released on September 8th, restored the original behavior: on its own, the variable is ignored again. There is nothing to fix in your config. Upgrading is the whole answer. The primary source is the claude-code v2.1.266 release notes.
| Version | CLAUDE_CODE_USE_GATEWAY set on its own |
|---|---|
| 2.1.264 and earlier | Ignored (only takes effect alongside ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN) |
| 2.1.265 | Forces gateway sign-in on its own, so existing setups fail every request |
| 2.1.266 and later | Ignored again |
If that were all, this would be a short notice. What actually held my attention was not the fix. It was the place people reach for first when something breaks.
We suspect the config because three inputs converge on one point
Claude Code authentication arrives from three directions at once: environment variables, settings.json, and apiKeyHelper. On top of that, .claude/settings.json exists at both the user and project level, and shell profiles differ from machine to machine.
Which means there is a decent chance that something you do not remember setting is currently in effect. An internal onboarding doc added a line to a shared shell profile. An export from an old experiment never got cleaned up. When the variable is undocumented, as it was here, it is even less likely to be in anyone's memory.
For a while my instinct in these moments was to start editing settings.json and see what happened. That did not serve me well. I thought I was hunting for the reason it broke, and instead I was slowly dismantling the part that had been correct all along.
What deserves suspicion is not the config by itself, but the pairing of config and version. Look at only one side and you will spend the afternoon repairing the side that never moved.
A script that puts the auth input surface on one page
So I keep a twenty-line script around. It does not print values. It prints what is present.
#!/usr/bin/env bash
# authsurface.sh — put the Claude Code auth input surface on one page
set -u
mask() {
local v="${1:-}"
[ -z "$v" ] && { echo "(unset)"; return; }
local n=${#v}
if [ "$n" -le 8 ]; then echo "set(len=$n)"; else echo "set(len=$n, tail=${v: -4})"; fi
}
# Version goes first on purpose — it becomes the axis when you diff later
echo "version=$( { claude --version 2>/dev/null || echo 'claude-not-found'; } | head -1 )"
# Secrets: presence and length only
for k in ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN; do
echo "$k=$(mask "${!k:-}")"
done
# Values you actually need to read
for k in ANTHROPIC_BASE_URL ANTHROPIC_CUSTOM_HEADERS CLAUDE_CODE_USE_GATEWAY \
CLAUDE_CODE_USE_BEDROCK CLAUDE_CODE_USE_VERTEX; do
echo "$k=${!k:-(unset)}"
done
# For settings files, "exists" is not the question — "what is in it" is
for f in "$HOME/.claude/settings.json" "./.claude/settings.json" "./.claude/settings.local.json"; do
if [ -f "$f" ]; then
helper=$(python3 -c "import json,sys;print(json.load(open(sys.argv[1])).get('apiKeyHelper','(none)'))" "$f" 2>/dev/null || echo "(parse-error)")
force=$(python3 -c "import json,sys;print(json.load(open(sys.argv[1])).get('forceLoginMethod','(none)'))" "$f" 2>/dev/null || echo "(parse-error)")
echo "settings:$f apiKeyHelper=$helper forceLoginMethod=$force"
else
echo "settings:$f (absent)"
fi
doneOn a machine sitting behind a gateway, the output looks like this.
version=2.1.260 (Claude Code)
ANTHROPIC_API_KEY=set(len=23, tail=1234)
ANTHROPIC_AUTH_TOKEN=(unset)
ANTHROPIC_BASE_URL=https://llm-gw.internal.example/v1
ANTHROPIC_CUSTOM_HEADERS=(unset)
CLAUDE_CODE_USE_GATEWAY=1
CLAUDE_CODE_USE_BEDROCK=(unset)
CLAUDE_CODE_USE_VERTEX=(unset)
settings:/home/you/.claude/settings.json (absent)
settings:./.claude/settings.json (absent)
settings:./.claude/settings.local.json (absent)With that page in hand, this particular regression is visible at a glance. CLAUDE_CODE_USE_GATEWAY=1 is present while ANTHROPIC_AUTH_TOKEN is (unset) — the exact combination that 2.1.264 quietly ignored, still sitting there.
One note on secrets. The mask wrapper exists so you can paste this output into an issue or a team channel without thinking twice. Length plus the last four characters is enough to answer "did the key get swapped for a different one," and nothing beyond that is needed. For the full landscape of variables, the Claude Code environment variables reference covers far more ground than this script does.
Storing version and config together turns triage into seconds
I save the output with a timestamp and compare it against the previous one. The moment to run it is right after upgrading.
#!/usr/bin/env bash
# authsnap.sh — save the auth input surface and diff it against the last one
set -u
DIR="${AUTHSNAP_DIR:-$HOME/.claude-authsnap}"
mkdir -p "$DIR"
NOW="$DIR/$(date +%Y%m%d-%H%M%S).txt"
"$(dirname "$0")/authsurface.sh" > "$NOW"
PREV=$(ls -1 "$DIR"/*.txt 2>/dev/null | grep -v "$(basename "$NOW")" | tail -1)
[ -z "${PREV:-}" ] && { echo "(nothing to compare yet — diffs start next run)"; exit 0; }
if diff -u "$PREV" "$NOW" > /tmp/authsnap.diff; then
echo "RESULT: input surface unchanged"
else
CHANGED=$(grep -cE '^[+-][A-Za-z]' /tmp/authsnap.diff)
VERONLY=$(grep -E '^[+-][A-Za-z]' /tmp/authsnap.diff | grep -cv '^[+-]version=')
if [ "$VERONLY" -eq 0 ]; then
echo "RESULT: only the version changed (config is identical)"
else
echo "RESULT: the config side moved too (${CHANGED} lines)"
fi
sed -n '4,40p' /tmp/authsnap.diff
fiRunning it twice with an upgrade in between produces this.
compare: 20260910-150742.txt -> 20260910-150744.txt
RESULT: only the version changed (config is identical)
-version=2.1.260 (Claude Code)
+version=2.1.265 (Claude Code)
ANTHROPIC_API_KEY=set(len=23, tail=1234)
ANTHROPIC_AUTH_TOKEN=(unset)
ANTHROPIC_BASE_URL=https://llm-gw.internal.example/v1One line of diff, and that one line is your evidence that you changed nothing. If ANTHROPIC_BASE_URL had moved as well, the verdict flips to the config side moved too, and so does the thing you go looking at.
The verdict is deliberately binary. When someone reads a log during an outage, the question they want answered before any of the details is whether this is their fault. Settle that first and the rest can be read calmly.
Putting version at the top of the output serves the same purpose. diff reports in line order, so a version change always surfaces as the first line you see.
The three places to look when the version was not the cause
When the verdict comes back as the config side moved too, this is the order I work through.
forceLoginMethodpinned in managed settings. With"gateway"in place, a leftover API key or a claude.ai login is ignored and/loginbecomes mandatory. Bedrock, Vertex AI, and Foundry sessions are unaffected.- What
apiKeyHelperactually returns. The helper can exit cleanly and still hand back an empty string or a value with a trailing newline. Runbash -c "$(your helper)" | od -c | headand read all the way to the end. - Key spelling. A single wrong character in a
settings.jsonkey is ignored without a word, which I wrote about separately in a one-letter typo in settings.json.
All three present identically: authentication, and only authentication, stops working. That symmetry is exactly why having the input surface on one page ahead of time pays for itself.
If you run unattended jobs, the related failure mode where credentials get overwritten mid-run is covered in one transient 401 replaced my long-lived token. That one may matter more to you than this one does.
For today, run authsurface.sh once on the machine you actually work from and keep the output. The diff becomes useful the moment you next upgrade. I have made a habit of spending that extra few seconds every time I bump the version.
And if your morning ends up saved by nothing more than a version bump, I'll count that as a good outcome for both of us.