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

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.
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%">
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.
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%">
ccswitch switch → a toast confirms; ✕ closes without switchingYou 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.
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.
A Claude Code mod (hooks/register.tsx, ~80 lines):
session.start and turn.complete run ccswitch list and parse the accounts.ui.render on PromptHint draws the account (or the open list) in the hint slot under the prompt.ccswitch switch <n> and refreshes.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.
hooks/register.tsx 81 lines1import { 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}
81types/index.d.ts 9 lines1/** 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