SLOPSHOPPER

quickswitch

One-click Claude account switching from the status line, powered by ccswitch

newspinnertoastprocess
v0.1.0MITupdated 2026-10-02Rocha101/claude-code-quickswitch
A shopper browsing a rack in a slop shop
README

quickswitch

Switch Claude accounts in one click — from the Claude Code status line.

Work seat, personal seat, the team account: one click on your email and pick.

MIT Claude Code mod powered by ccswitch Stars

English · Português

<img src="assets/demo.gif" alt="The account next to the mode label opens into a list of accounts with their token status" width="100%">

Why

Hitting the usage limit on one seat while another sits idle is the worst. ccswitch already swaps Claude Code credentials without the log-out/log-in dance — quickswitch puts it one click away, right where you're looking.

What you get

Your current account lives in the status line, next to the mode label:

<img src="assets/closed.png" alt="auto mode on · ⇄ ada@lovelace.dev" width="100%">

Click it and the same spot turns into every account you manage, with how long each token has left:

<img src="assets/open.png" alt="the account list: active highlighted, expired dimmed" width="100%">

  • ● the active account, highlighted · ○ the others · expired tokens dimmed
  • click one → ccswitch switch → a toast confirms; ✕ closes without switching
  • the list refreshes on session start, after each switch and every turn — switches made in another terminal show up too
  • zero extra rows: no band, no panel, it is part of the status line

Install

You need ccswitch with your accounts added (ccswitch add).

curl -fsSL https://raw.githubusercontent.com/Rocha101/claude-code-quickswitch/main/install.sh | bash

Then open a new Claude Code session.

git clone https://github.com/Rocha101/claude-code-quickswitch ~/.claude/mods/claude-code-quickswitch

Add the folder to CLAUDE_CODE_PLUGIN_DIRS in the env block of ~/.claude/settings.json (:-separated), or start Claude Code with claude --plugin-dir ~/.claude/mods/claude-code-quickswitch.

Requirements: Claude Code 2.1.287+ (mods), ccswitch, jq, git. Clicking needs a terminal that reports the mouse (Claude Code's fullscreen mode); without one, ctrl+x tab focuses it and Enter presses.

Good to know

A switch applies on restart. ccswitch swaps the stored credentials; a running Claude Code keeps the old session until it restarts. After switching: /exit, then claude -c to continue right where you were. The toast reminds you.

If you also print your email in a custom status line script, drop it — quickswitch already shows it.

How it works

A Claude Code mod (hooks/register.tsx, ~80 lines):

  1. session.start and turn.complete run ccswitch list and parse the accounts.
  2. ui.render on PromptHint draws the account (or the open list) in the hint slot under the prompt.
  3. A press runs ccswitch switch <n> and refreshes.

Contributing

claude plugin validate .
claude plugin test .

Issues and PRs welcome. If quickswitch rescued you from a rate limit, leave a star ⭐

See also: glance — images and rendered HTML inline in Claude Code.

License

MIT © Rocha101 · account switching by ccswitch

Source 2 files
hooks/register.tsx 81 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Account } from '../types'
5
6const accounts = atom({ plugin: 'quickswitch', key: 'accounts' } as const, [])
7const isOpen = atom({ plugin: 'quickswitch', key: 'isOpen' } as const, false)
8
9// `▶  1  me@x.com  [~7h]  (active)`, colours stripped
10const ROW = /^\s*(▶)?\s*(\d+)\s+(\S+@\S+)\s+\[([^\]]*)\]/
11
12async function refresh($: EngineInterface) {
13  const r = await $.process.run(['ccswitch', 'list'], { env: { NO_COLOR: '1' } })
14  if (r.exitCode) return
15  const list: Account[] = []
16  for (const line of r.stdout.replace(/\x1b\[[0-9;]*m/g, '').split('\n')) {
17    const m = ROW.exec(line)
18    if (m) list.push({ n: Number(m[2]), email: m[3]!, token: m[4]!, isActive: Boolean(m[1]) || line.includes('(active)') })
19  }
20  await update($, accounts, () => list)
21}
22
23async function switchTo($: EngineInterface, a: Account) {
24  await update($, isOpen, () => false)
25  if (a.isActive) return
26  const r = await $.process.run(['ccswitch', 'switch', String(a.n)], { env: { NO_COLOR: '1' } })
27  // ccswitch swaps the stored credentials; a running Claude Code picks them up on restart
28  $.ui.toast(
29    r.exitCode
30      ? `ccswitch failed: ${(r.stderr || r.stdout).trim().slice(-120)}`
31      : `switched to ${a.email} · restart Claude Code to use it (/exit, then claude -c)`,
32    { timeoutMs: 8000 },
33  )
34  await refresh($)
35}
36
37export const register: Register = on => {
38  on('session.start', async ($, e, next) => {
39    const started = await next(e)
40    await refresh($)
41    return started
42  })
43
44  // tokens expire and other terminals switch too: re-read once a turn
45  on('turn.complete', async ($, e, next) => {
46    const done = await next(e)
47    await refresh($)
48    return done
49  })
50
51  // lives on the hint line under the prompt, right after the mode label, so it reads as part
52  // of the status line; open, the same spot turns into the account list
53  on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
54    const list = await read($, accounts)
55    if (list.length === 0) return next(e)
56    const active = list.find(a => a.isActive)
57    const open = await read($, isOpen)
58    const { Box, Button } = $.ui.resolve(e)
59    if (open) {
60      return (
61        <Box flexDirection="row" columnGap={2} flexWrap="wrap">
62          {list.map(a => (
63            <Button
64              key={`acc${a.n}`}
65              plain
66              label={`${a.isActive ? '●' : '○'} ${a.email} ${a.token}`}
67              variant={a.isActive ? 'primary' : undefined}
68              dimColor={a.token === 'expired'}
69              onPress={() => switchTo($, a)}
70            />
71          ))}
72          <Button key="close" plain dimColor label="✕" onPress={() => update($, isOpen, () => false)} />
73        </Box>
74      )
75    }
76    // same spot as the open list (right after the mode label): the account alone, in place of the
77    // engine's hint, which claims the whole row and pushes anything beside it to a new line
78    return <Button key="who" plain dimColor label={`⇄ ${active?.email ?? 'no account'}`} onPress={() => update($, isOpen, () => true)} />
79  })
80}
81
types/index.d.ts 9 lines
1/** One account as `ccswitch list` prints it. */
2export type Account = { n: number; email: string; token: string; isActive: boolean }
3
4declare module 'claude-code' {
5  interface PluginState {
6    quickswitch: { accounts: Account[]; isOpen: boolean }
7  }
8}
9