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.
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.
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.
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.aiIN 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-tokenPROPOSED
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 tokens — Invalid 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.