Every figure measured live on the mini
Verified against claude agents · ccswitch · the shipped binary  ·  2026-07-31

Should we move auth to
one-year tokens?

What broke, what a setup-token actually fixes,
and the one thing it does not fix at all.
VERDICT  ·  YES FOR CI, NO FOR THE FLEET
docs.propflowai.coAUTH · SETUP-TOKENS
02 / VERDICT
Decide from this slide

Mint the tokens. Point CI at them. Leave the fleet alone.

One-year tokens end the 8-hour expiry deaths and the refresh race outright. They add no quota, and they do not rotate into a running session.

MINTED
6
one per account, all working
VALID FOR
1yr
vs 8 hours today
USABLE NOW
3/6
the other 3 are out of quota
EXTRA CAPACITY
0
tokens are not quota
The rate limits are a capacity problem, not an auth problem. No credential change fixes ten simultaneously-busy agents sharing one subscription window.
measured live · 2026-07-3102
03 / CONTEXT
Why our experience differs from everyone else's

We run this far outside the envelope it was designed for.

Most people run one to three Claude Code sessions on one account and never switch. Every failure below is invisible at that scale.

LIVE SESSIONS
37
one machine, one daemon
BUSY AT ONCE
10
all drawing on one window
ACCOUNTS
6
rotated to dodge limits
TYPICAL USER
1–3
and never rotates
Three behaviours that are individually fine combine badly here: sessions cache credentials, we rotate accounts, and we live at the rate limit.
claude agents --json · 2026-07-3103
04 / TODAY
The current method, end to end

One login per machine — then an 8-hour clock
that something has to keep resetting.

/login once
macOS Keychain
8h access token
expires
refresh grant
What is designed to happen

Log in once, everywhere

Credentials are stored machine-wide. Every session started afterwards reads them at startup. You are never meant to log in per session.
What we added on top

A 2-hourly rotator

rotate-reviewer-tokens.sh refreshes the bank and rewrites six token files via launchd, because bank access tokens expire in about eight hours.
code.claude.com/docs/en/authentication · rotate-reviewer-tokens.sh:1-1004
05 / TODAY
All three are upstream, none are ours

Three things break, and only one of them is about tokens

01 · CACHE

A running session holds its credential in memory

A login in one session never reaches another. Measured: login in 147d127b at 13:16:33 · session f6611d7c still 429-ing at 13:16:49 and 13:17:06.
02 · KEYCHAIN

SSH and tmux cannot reach the login Keychain

The Security Framework wants a GUI session to authorize. Issues #9403 · #29816 · #44089. Already fixed on our mini — see slide 14.
03 · RACE

Concurrent sessions race on a single-use refresh token

No file locking. The loser gets a 404 and loses auth with no automatic recovery. Issues #48786 · #27933 · #56339 · #24317 · #25609.
Only the third one scales with session count — which is why it is our problem and almost nobody else's.
github.com/anthropics/claude-code — issue tracker05
06 / TODAY
The one that actually costs us logins

Thirty-seven processes, one single-use refresh token,
and no lock.

The mechanism
When one session refreshes, it writes new access and refresh tokens. Every other session is holding the old pair.
The loser
Gets a 404 and loses authentication with no automatic recovery.
Documented workaround
"Close stale Claude Code sessions to reduce the number of concurrent processes." We run 37.
Net effect
Re-login prompts that look random, are blamed on rate limits, and scale with how many agents are running.
Claude Code does have a mitigation — "adopting sibling-rotated access token from credentials file"but it reads the credentials file, which on this machine had gone nine hours stale.
issues #48786 #27933 #56339 · strings(2.1.220)06
07 / TODAY
Written down in our own code, months ago

The CI reviewer has been dying every eight hours
and reporting it as something else.

What the log says

"Rate-limit capped"

The workflow prints "Every configured Claude account is rate-limit capped" — which reads as a quota problem and gets ignored.
What is actually happening

Six HTTP 401s

The token slots went stale about eight hours after someone last set them by hand. An expired token, misreported as a cap.
Nothing needs to move. The reviewer already reads CLAUDE_CODE_OAUTH_TOKEN and already sends Authorization: Bearer — the exact variable and transport a setup-token uses. The only change is the token's lifetime: 8 hours becomes a year, and the 2-hourly rotator stops being load-bearing.
~/.claude/scripts/rotate-reviewer-tokens.sh:12-1807
08 / PROPOSAL
One command, one browser round, once a year

A one-year token removes the refresh cycle entirely.

Today

8-hour OAuth access token

Fleet: read from the Keychain at startup, refreshed by a grant 37 processes race on. CI: already a Bearer env var — but fed an 8h token, so it dies unless launchd rewrites it every 2h.
With setup-token

1-year OAuth token

Read from an env var · no refresh grant, so no race · no Keychain, so headless and SSH-safe · nothing to rotate every two hours.
Transport and lifetime are different things. "Bearer" is how a token is sent; "long-lived" is how long it lasts. CI already sends Bearer — it just sends an 8-hour token that way. So for CI this is a one-line value swap; for the fleet it is a real switch, since CLAUDE_CODE_OAUTH_TOKEN outranks the subscription login.
code.claude.com/docs/en/authentication#generate-a-long-lived-token08
09 / TAXONOMY
What each is called — and note that "Bearer" is a transport, not a lifetime

Five ways to authenticate. Only one other is worth moving to.

01 · /login

Subscription login — claude.ai  IN USE

Pro: full scope — MCP, connectors, Remote Control · covers every session started after it.  Con: 8h token · refresh grant 37 processes race on · needs the Keychain · never reaches a running session.
02 · CLAUDE_CODE_OAUTH_TOKEN

Long-lived token — from claude setup-token  PROPOSED

Pro: valid 1 year · no refresh grant, so no race · no Keychain, so CI-safe.  Con: scope user:inference only · MCP unresolved · still read once at startup.
03 · ANTHROPIC_AUTH_TOKEN

Bearer env var — carries any lifetime

Pro: same Bearer path · already how the TS app authenticates.  Con: inherits whatever lifetime you feed it — an 8h token stays an 8h problem · silently overrides /login.
04 · apiKeyHelper

Rotating credential script

Pro: the only source re-invoked mid-session (every 5 min / on 401) — real live rotation.  Con: sends x-api-key, so it rejects OAuth tokensInvalid API key. Unusable for us.
05 · ANTHROPIC_API_KEY

Metered API key

Pro: a separate quota pool — no 5h or weekly cap.  Con: billed per token in dollars · retired at PropFlow since 2026-06-05.
precedence per code.claude.com/docs/en/authentication · tested 2026-07-3109
10 / EVIDENCE
Done on 2026-07-31, one browser round each

All six accounts now have a one-year token.

Stored 0600 in ~/.claude/.setup-tokens/<email>.token with an index.json ledger recording mint date, expiry and a sha. All six expire 2027-07-31.

MINTED
6/6
gera · agentsmith · evals
lab · morpheus · test
DISTINCT
6
no accidental duplicates
AUTHENTICATED
6/6
every failure was quota
WIRED UP
0
nothing points at them yet
Do not confuse these with ~/.claude/.tokens/account{1..6}.token — those are the 8-hour tokens the rotator overwrites every two hours. Anything written there is gone within 120 minutes.
~/.claude/.setup-tokens/index.json10
11 / EVIDENCE
One-word call per token · negligible spend

Every token authenticates. Three have room to spend.

OK · EVALS

propflow.evals@gmail.com

Replied normally · weekly 11% used.
OK · LAB

propflow.lab@gmail.com

Replied normally · weekly 89% used.
OK · TEST

propflow.test@gmail.com

Replied normally · weekly 80% used.
CAP · GERA

gera@propflowai.co

Weekly limit · resets Aug 2, 7pm. Authenticated fine — simply spent.
CAP · MORPH

propflow.morpheus@gmail.com

Weekly limit · resets Aug 2, 9pm.
CAP · SMITH

propflow.agentsmith@gmail.com

Session limit · resets 3:10pm same day.
6 × claude -p, CLAUDE_CODE_OAUTH_TOKEN · 2026-07-3111
12 / TRADE-OFF
The case for

Four failure modes disappear, permanently.

Gone

The refresh race

No refresh grant exists, so 37 processes cannot race on one. This is the fix that scales with the fleet.
Gone

The 8-hour death

A year of validity instead of eight hours. The 2-hourly rotator stops being load-bearing for anything pointed at these.
Gone

The Keychain dependency

An env var needs no GUI session, so SSH, launchd and CI all work without the unlock dance.
Reversible in one step. Unset the variable and the session falls straight back to the subscription login — nothing is migrated, nothing is destroyed.
verified live · 2026-07-3112
13 / TRADE-OFF
The case against — read this before rolling out

Three real costs, and one open question.

Cost

Narrow scope

Scope is user:inference only — no Remote Control, no claude.ai connectors. MCP behaviour is unresolved: see below.
Cost

Still no live rotation

Read once at process start, exactly like today. Changing accounts still never reaches a session that is already running.
Cost

Attribution is manual

Nothing can tell you which account a token belongs to. Whoever mints it must record it — and our first one was mislabelled.
Open question — MCP. A claude -p run on the token saw 0 MCP tools where the same command on the normal login saw 32+. But a real background session on the token loaded its servers and reported only "1 MCP server needs authentication". Not yet resolved — settle it before moving any MCP-dependent session.
measured both ways · 2026-07-31 · unresolved13
14 / FINDINGS
Something that works, that we assumed didn't

The "needs a GUI session" limitation does not apply to this machine.

Tested from a background job over SSH — the exact context that is supposed to be blocked:

LOCK STATE
open
no-timeout
READ CREDENTIAL
rc 0
the real item
WRITE ITEM
rc 0
and read back
DELETE
rc 0
full access
So ccswitch switch refusing over SSH is its own guard, not the OS. It infers from the environment instead of testing the Keychain — a small, contained fix that would let account rotation run from the CLI.
security find/add/delete-generic-password · bg job over SSH14
15 / FINDINGS
Each of these was tried and measured

Four dead ends, so nobody spends a day on them again.

apiKeyHelper
The only credential source re-invoked mid-session (every 5 min, or on 401) — so it looked like live rotation. It sends x-api-key and rejects OAuth tokens: Invalid API key. Verified.
Fanning out /login
Not automatable. Choosing "Claude account with subscription" opens a full browser OAuth flow with a code to paste.
Restarting sessions
Works, but mints a new session id, drops in-flight work, and a lingering claude attach viewer revives the stopped session as a duplicate.
Waiting for a 429 to heal
A 401 makes a session re-read the store. A 429 does not. A rate-limited session stays pinned to the spent account.
all four measured live · 2026-07-3115
16 / FINDINGS
The bug that made rotation feel broken

Two accounts read "0% used" while being
completely spent for the week.

What the 5-hour window says
0%
gera and morpheus both looked wide open — the field every picker reads.
What the weekly cap says
100%
Morpheus's very first call: weekly limit · resets Aug 2 at 9pm.
Anything that picks "the best account" by the 5-hour number will confidently choose a dead one. That includes ccswitch --best and the gate we had written into the restart tool. Four of six accounts were at or near their weekly cap while showing 5h headroom.
~/.claude/ccswitch/cache.json vs live calls · 2026-07-3116
17 / CORRECTIONS
On the record, so the next session doesn't repeat them

Five things I asserted during this work that were wrong.

01 · TOOL

Built a restart tool that made things worse

It restarted sessions onto the same exhausted account and left nine duplicate sessions. Removed.
02 · CLAIM

"A login should propagate to everyone"

It does not. What looks like propagation is the rate-limit window resetting for every session at once.
03 · METHOD

Doubted a correct finding on a broken test

Grepped a Mach-O binary for strings and got zero hits for everything — including oauth. The method was broken, not the claim.
04 · SIGNAL

Called a token's self-reported email reliable

It reads the machine-wide identity file, not the token. It was right once by coincidence.
05 · SPEND

Started burning quota to satisfy curiosity

Four long generations to identify an account, on a rate-limited machine. Stopped by the user, correctly.
2026-07-31 session log17
18 / ACTION
Narrow, reversible, and worth doing

Move CI. Leave the fleet. Fix the picker.

Do

Point CI at the tokens

The reviewer dies every ~8h on expired slots and needs no MCP. This ends it outright and retires the rotator for those slots.
Don't

Move the interactive fleet

MCP behaviour under the token is unresolved. Smith runs on Slack, WhatsApp and Workspace — settle slide 13 first.
Fix

Score on the weekly cap

Any "best account" logic must read the weekly number, not the 5-hour one. This is the cheapest real improvement on the list.
And accept the part no token fixes: ten simultaneously-busy agents on one subscription window will hit limits. Fewer, longer-lived sessions would do more than any credential change.
recommendation · 2026-07-3118
19 / CLOSE

One-year tokens fix the clock.
They do not fix the capacity.

Six tokens are minted, verified and wired to nothing.
Move CI onto them; leave the fleet where it is until the MCP question is settled.
docs.propflowai.co/a/auth-setup-tokensPROPFLOW · AUTH

All slides

PropFlow Docs