<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 01Jam</title>
    <description>The latest articles on DEV Community by 01Jam (@01jam).</description>
    <link>https://dev.to/01jam</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F785046%2F8755c795-f54b-451d-84c3-7526449cb61d.png</url>
      <title>DEV Community: 01Jam</title>
      <link>https://dev.to/01jam</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/01jam"/>
    <language>en</language>
    <item>
      <title>Switching between Claude Code accounts without the sign-out dance</title>
      <dc:creator>01Jam</dc:creator>
      <pubDate>Mon, 31 Aug 2026 17:21:30 +0000</pubDate>
      <link>https://dev.to/01jam/switching-between-claude-code-accounts-without-the-sign-out-dance-3n69</link>
      <guid>https://dev.to/01jam/switching-between-claude-code-accounts-without-the-sign-out-dance-3n69</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;🤖 &lt;strong&gt;Written by an AI.&lt;/strong&gt; I gave Claude an outline of the points I wanted made&lt;br&gt;
and it wrote the prose. I have read it, checked it against the code, and I&lt;br&gt;
stand behind every claim in it — but the sentences are not mine, and it seemed&lt;br&gt;
wrong not to say so, given what the article is about.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I have three Claude accounts. One for client work, one personal, one that&lt;br&gt;
belongs to a project. Claude Code has room for exactly one of them at a time,&lt;br&gt;
and moving between them means &lt;code&gt;claude logout&lt;/code&gt;, &lt;code&gt;claude&lt;/code&gt;, a browser tab, a code&lt;br&gt;
to paste, and — because I use the VSCode extension too — a small prayer that&lt;br&gt;
both ends noticed.&lt;/p&gt;

&lt;p&gt;I did that maybe six times a day for a week before writing&lt;br&gt;
&lt;a href="https://01jam.github.io/claude-account-switcher/" rel="noopener noreferrer"&gt;Claude Account Switcher&lt;/a&gt;: a&lt;br&gt;
desktop app for Linux and macOS that keeps every login saved and puts the one&lt;br&gt;
you want back in place, from a window or from the icon in the tray.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffzgp1br71n4za89pa4jt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffzgp1br71n4za89pa4jt.webp" alt="The app window: three saved accounts, each with a five-hour and a weekly usage meter, the active one highlighted" width="800" height="1585"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The rest of this post is what I learned building it, which turned out to be&lt;br&gt;
more interesting than the app itself.&lt;/p&gt;
&lt;h2&gt;
  
  
  What a Claude Code login actually is
&lt;/h2&gt;

&lt;p&gt;Two pieces, and that is the whole reason this is possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;the OAuth tokens.&lt;/strong&gt; On Linux, &lt;code&gt;~/.claude/.credentials.json&lt;/code&gt;. On macOS, an
entry in the login keychain under the service &lt;code&gt;Claude Code-credentials&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;~/.claude.json&lt;/code&gt;&lt;/strong&gt;, of which only a handful of keys identify the account —
&lt;code&gt;oauthAccount&lt;/code&gt;, &lt;code&gt;userID&lt;/code&gt;, and friends. Everything else in that file is your
projects, your history, your preferences.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a "profile" is a copy of the first and a surgical copy of the second.&lt;br&gt;
Switching is putting them back. The app keeps each one under&lt;br&gt;
&lt;code&gt;~/.config/claude-switch/profiles/&amp;lt;id&amp;gt;/&lt;/code&gt; (&lt;code&gt;~/Library/Application Support/&lt;/code&gt; on&lt;br&gt;
macOS), writes atomically, and drops a snapshot in &lt;code&gt;backups/&lt;/code&gt; on every switch.&lt;/p&gt;

&lt;p&gt;Two things I got wrong before I got them right:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rest of &lt;code&gt;~/.claude.json&lt;/code&gt; must survive.&lt;/strong&gt; The first version wrote the file&lt;br&gt;
whole, from the profile. That is a fine way to lose every project entry you had.&lt;br&gt;
Now only the account keys are merged in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tokens rotate while you work.&lt;/strong&gt; Claude Code refreshes its access token as it&lt;br&gt;
runs, so the copy you saved an hour ago is already stale. Right before every&lt;br&gt;
switch, the app copies the &lt;em&gt;live&lt;/em&gt; credentials back into the profile it is&lt;br&gt;
leaving. Without that step, you save a token that has already moved on and the&lt;br&gt;
account looks broken the next time you come back to it.&lt;/p&gt;

&lt;p&gt;Nice side effect of working at this level: one switch covers both the CLI and&lt;br&gt;
the VSCode extension, because they read the same two things.&lt;/p&gt;
&lt;h2&gt;
  
  
  Showing what each account has left
&lt;/h2&gt;

&lt;p&gt;Claude Code has a &lt;code&gt;/usage&lt;/code&gt; command, and it gets its numbers from&lt;br&gt;
&lt;code&gt;GET https://api.anthropic.com/api/oauth/usage&lt;/code&gt;, called with your OAuth token.&lt;br&gt;
It answers with two rolling windows — the five-hour session and the week — as&lt;br&gt;
utilisation percentages plus reset times.&lt;/p&gt;

&lt;p&gt;That is not a public API. It is undocumented, its shape can change without&lt;br&gt;
notice, and it is rate limited in a way nobody has written down. Every field is&lt;br&gt;
therefore optional in the client: if one is missing, the meter shows &lt;code&gt;—&lt;/code&gt; and&lt;br&gt;
nothing downstream fires.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyzao67pwqu2r69y60n8l.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyzao67pwqu2r69y60n8l.webp" alt="One account card: the five-hour session at 72% with a threshold marker at 90%, and the week at 38% with a threshold at 85%" width="799" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each meter carries a draggable marker: the threshold past which the account&lt;br&gt;
counts as spent. Turn auto-switch on and the app moves to the next account in&lt;br&gt;
list order as soon as either counter crosses its own line. Default is 100%, so&lt;br&gt;
out of the box nothing happens until you genuinely hit the limit.&lt;/p&gt;
&lt;h2&gt;
  
  
  The part I actually want to talk about: asking politely
&lt;/h2&gt;

&lt;p&gt;The first version polled every account once a minute. Sixty requests an hour per&lt;br&gt;
account. It earned exactly the &lt;code&gt;429&lt;/code&gt;s you would predict, and then it did the&lt;br&gt;
worst possible thing with them: it blanked every meter and stalled the&lt;br&gt;
auto-switch, right at the moment the user needed to know which account had room.&lt;/p&gt;

&lt;p&gt;Fixing that properly needed a number nobody publishes. I did not want to&lt;br&gt;
rediscover it by brute force against someone else's endpoint, and it turns out I&lt;br&gt;
did not have to — &lt;a href="https://github.com/realiti4/claude-swap" rel="noopener noreferrer"&gt;realiti4/claude-swap&lt;/a&gt;&lt;br&gt;
had already probed it deliberately and written down the method, the dates, and&lt;br&gt;
the result in a file called &lt;code&gt;poll_policy.py&lt;/code&gt;. What that work found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;roughly &lt;strong&gt;28–30 requests per hour per identity&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;over a &lt;strong&gt;trailing 60-minute window&lt;/strong&gt;, not a bucket that refills — so a burst
saturates for a full hour, and going quiet does not hand the headroom back
early&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The constants in my &lt;code&gt;pace.rs&lt;/code&gt; are theirs. Credit where it is due, and if you are&lt;br&gt;
building something that touches this endpoint, go and read their notes rather&lt;br&gt;
than measuring it again.&lt;/p&gt;

&lt;p&gt;From there each account gets its own schedule:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;when&lt;/th&gt;
&lt;th&gt;interval&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;the floor, and while a reading still counts as fresh&lt;/td&gt;
&lt;td&gt;3 min (~20/hour against a cap near 30)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the active account, once nothing is moving&lt;/td&gt;
&lt;td&gt;decays to 5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;another saved account, idle&lt;/td&gt;
&lt;td&gt;decays to 10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;an account already spent&lt;/td&gt;
&lt;td&gt;10 min — slow, never abandoned: a grant can free it early&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the active account within 15 points of its threshold &lt;strong&gt;and&lt;/strong&gt; actually moving&lt;/td&gt;
&lt;td&gt;1 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;after a refusal&lt;/td&gt;
&lt;td&gt;6 min floor, backing off ×1.5 toward 30 min while they recur&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few decisions inside that table that took longer than they look:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The one-minute case is bounded by construction, not by a timer.&lt;/strong&gt; It only&lt;br&gt;
applies when the active account is close to its threshold &lt;em&gt;and&lt;/em&gt; its numbers&lt;br&gt;
changed since last time. Either the threshold gets crossed and the auto-switch&lt;br&gt;
moves away, or the movement stops and the next plan decays back to the floor.&lt;br&gt;
There is no state to reset and nothing to leak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backoff is multiplicative because the budget is shared and invisible.&lt;/strong&gt; Your&lt;br&gt;
other laptop is watching the same account, neither process can see the other, and&lt;br&gt;
the endpoint reports no remaining count. That is the situation TCP is in, and it&lt;br&gt;
is why TCP backs off the way it does. Every interval also carries ±10% of jitter&lt;br&gt;
so two machines drift apart instead of arriving together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A refusal is about one account, not all of them.&lt;/strong&gt; The endpoint answers for the&lt;br&gt;
token it was handed. A &lt;code&gt;429&lt;/code&gt; almost always means &lt;em&gt;that&lt;/em&gt; account is at its limit —&lt;br&gt;
which is precisely the account the user is about to switch away from. Pausing the&lt;br&gt;
others would blank the meters of the accounts they are switching &lt;em&gt;to&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Retry-After&lt;/code&gt; is a suggestion here.&lt;/strong&gt; The endpoint answers refusals with a&lt;br&gt;
stock &lt;code&gt;retry-after: 3600&lt;/code&gt; and is then perfectly willing twenty minutes later. The&lt;br&gt;
cooldown is capped at 15 minutes, because past that the numbers are too old for&lt;br&gt;
the auto-switch to act on anyway — so 15 minutes is as long as it is worth&lt;br&gt;
sitting blind before spending one request to find out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cache lives on disk, cooldowns included.&lt;/strong&gt; Held only in memory, every&lt;br&gt;
restart began blind and re-asked for everything at once — which is a burst, and a&lt;br&gt;
burst is the thing that saturates the hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Old numbers are shown, and labelled.&lt;/strong&gt; After a refusal the app keeps displaying&lt;br&gt;
the last readings it has rather than emptying the bars. But once a reading passes&lt;br&gt;
five minutes, the card says how old it is — "numbers from 3h ago" — and so does&lt;br&gt;
the tray menu, where a frozen number is easiest to mistake for a live one. Kept&lt;br&gt;
numbers that look freshly loaded are exactly how a spent account appears to have&lt;br&gt;
room left.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And nothing acts on a reading older than 15 minutes.&lt;/strong&gt; Showing an old number is&lt;br&gt;
fine. Rotating accounts on one is not: a five-hour window that has since reset&lt;br&gt;
still reads as full in the cache, and switching on that is a switch nobody asked&lt;br&gt;
for.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two features that fell out of the meters
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start on the freest account.&lt;/strong&gt; At launch, optionally, go to the account with&lt;br&gt;
the most week left. "Most" is a ratio, not a percentage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;room per day = (100 − week used) / days until the week resets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twenty points to make six days last (3.3 a day) is a tighter week than seventy&lt;br&gt;
points for one day (70), and the second account wins. Below a day the divisor&lt;br&gt;
stops falling, because a window resetting in ten minutes is not a reason to start&lt;br&gt;
on an account with nothing left in it &lt;em&gt;right now&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Renewing the tokens.&lt;/strong&gt; Claude Code owns the login &lt;em&gt;flow&lt;/em&gt;, but not the renewal:&lt;br&gt;
the refresh token it stores was issued to its own public client, so the same&lt;br&gt;
grant works from the app and produces exactly the credentials the CLI would have&lt;br&gt;
written. This matters because usage for a non-active account is read with its&lt;br&gt;
stored token, and a stored token that has expired means two blank meters on the&lt;br&gt;
account you might want to switch to.&lt;/p&gt;

&lt;p&gt;The interesting half is doing it without fighting the CLI. Claude Code takes a&lt;br&gt;
lock — &lt;code&gt;proper-lockfile&lt;/code&gt; semantics on &lt;code&gt;~/.claude/.storage-write.lock&lt;/code&gt;, a&lt;br&gt;
directory whose &lt;code&gt;mkdir&lt;/code&gt; decides who holds it — before writing its login. So the&lt;br&gt;
app takes the same lock, and re-reads the file &lt;em&gt;inside&lt;/em&gt; it: if a session renewed&lt;br&gt;
first, the app stands down rather than overwriting. A lock whose mtime has not&lt;br&gt;
moved for 15 seconds is treated as abandoned and cleared, which is also the cure&lt;br&gt;
for Claude Code's own&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Failed to refresh OAuth token: another Claude Code process is refreshing it or&lt;br&gt;
exited mid-refresh&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;left behind by a session that died mid-write. Renewing a token, unlike switching&lt;br&gt;
accounts, is safe while Claude Code is running.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it deliberately does not do
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Claude Desktop.&lt;/strong&gt; It is an Electron app that authenticates like a browser: the&lt;br&gt;
session is a &lt;code&gt;sessionKey&lt;/code&gt; cookie inside a Chromium profile in&lt;br&gt;
&lt;code&gt;~/.config/Claude/&lt;/code&gt;. Nothing in common with the files above. Covering it would&lt;br&gt;
mean closing Desktop on every switch and chasing its updates — so it stays on&lt;br&gt;
whichever account you signed it into. A choice, not a bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keyring storage.&lt;/strong&gt; The tokens sit in the clear with &lt;code&gt;0600&lt;/code&gt; permissions, exactly&lt;br&gt;
as Claude Code stores them itself. On macOS the live tokens are in the keychain,&lt;br&gt;
but the app's own copies are still files. This is not a secret manager and it&lt;br&gt;
does not pretend to be one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Switching while Claude Code is open.&lt;/strong&gt; The credentials go in under the CLI's&lt;br&gt;
lock, but &lt;code&gt;~/.claude.json&lt;/code&gt; is not covered by it, and a running session can&lt;br&gt;
rewrite the account keys there and quietly undo the switch. Close it first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Odds and ends
&lt;/h2&gt;

&lt;p&gt;The window draws its own title bar — no system decorations — which on Wayland&lt;br&gt;
meant discovering that starting a drag on &lt;code&gt;mousedown&lt;/code&gt; eats the &lt;code&gt;click&lt;/code&gt; that&lt;br&gt;
should follow, so buttons on the bar stop working. It starts on &lt;em&gt;movement&lt;/em&gt;&lt;br&gt;
instead: a press that stays put is still a click.&lt;/p&gt;

&lt;p&gt;The app also finds its own updates: every six hours it asks GitHub for the newest&lt;br&gt;
release, and a newer tag puts a dot on the gear and an offer at the top of&lt;br&gt;
Settings. Installing a &lt;code&gt;.deb&lt;/code&gt; or &lt;code&gt;.rpm&lt;/code&gt; hands it to the system package manager&lt;br&gt;
under &lt;code&gt;pkexec&lt;/code&gt;, so polkit takes the password and the app never sees it. The first&lt;br&gt;
attempt handed the file to the desktop instead, which fails exactly where it&lt;br&gt;
matters: on Ubuntu the default handler for a &lt;code&gt;.deb&lt;/code&gt; is the Snap Store, which will&lt;br&gt;
not install a local package at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmrvkat4ioaeyb4hy26cm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmrvkat4ioaeyb4hy26cm.webp" alt="The Settings dialog, with an available update at the top and four switches below it" width="800" height="1169"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Get it
&lt;/h2&gt;

&lt;p&gt;Linux &lt;code&gt;.deb&lt;/code&gt;, &lt;code&gt;.rpm&lt;/code&gt; and AppImage, plus a universal macOS &lt;code&gt;.dmg&lt;/code&gt;, on the&lt;br&gt;
&lt;a href="https://github.com/01jam/claude-account-switcher/releases" rel="noopener noreferrer"&gt;releases page&lt;/a&gt;.&lt;br&gt;
Source is MIT: &lt;a href="https://github.com/01jam/claude-account-switcher" rel="noopener noreferrer"&gt;01jam/claude-account-switcher&lt;/a&gt;.&lt;br&gt;
The macOS builds are unsigned — there is no Apple Developer certificate behind&lt;br&gt;
this project — so the first launch needs a one-time allow under System Settings →&lt;br&gt;
Privacy &amp;amp; Security.&lt;/p&gt;

&lt;p&gt;One more disclosure, in the spirit of the one at the top: the app itself is&lt;br&gt;
vibe coded. Not one line of it was typed by hand; it was prompted into existence&lt;br&gt;
with Claude Code and reviewed mostly by using it. It is small and self-contained&lt;br&gt;
enough that building it by hand would not have bought anything. Whether that&lt;br&gt;
bothers you is a fair thing to decide before installing something that handles&lt;br&gt;
your logins — the source is right there.&lt;/p&gt;

&lt;p&gt;It is an independent tool, not affiliated with or endorsed by Anthropic, and it&lt;br&gt;
reads an undocumented endpoint that may change or stop working without notice.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>linux</category>
      <category>rust</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
