Run each project on its own Claude account: shows the account, cost and limits above the prompt, switches the account per folder, and hands the next session…

Claude Code accounts and cost per client: run every project on the right Claude account, and always know what it costs.
Formerly Claude Multi-Account. Not affiliated with Anthropic; Claude and Claude Code are trademarks of Anthropic.
Freelancers and consultants often hold a Claude account per client, plus a personal one. Claude Code signs in once per config directory, so it is easy to spend a client's quota on your own project, or lose track of what a session costs. Billable puts the account, the usage and the switch where you can see them.


Add account creates a config directory for the new account, installs the companion plugin into it, and opens a terminal to sign in. An account you added earlier, or by hand, gets the plugin the next time VS Code starts, once the plugin runs on any of your accounts. An account where you turned the plugin off is left alone.Billable: Cost report by account.Share conversations, memory and settings with the default account links an account to the default one: conversations and memory, or everything but the sign-in. Each account keeps its own sign-in, plan and limits.Stop idle sessions stops the ones you pick and gives you the command to resume each one, and a setting can stop long-idle sessions on its own.Claude Code sessions stay in memory until you close them, and a dozen forgotten ones can hold several GB. Billable reads the session files Claude Code keeps in each account's config directory (sessions/<pid>.json) and the process list.
Claude sessions: 12 running · 8 idle · 3.5 GB RAM, with a Stop idle sessions button.Billable: Stop idle sessions lists the idle sessions with their folder, how long they have been quiet and their RAM. Sessions quiet longer than the threshold are already selected (the auto-stop setting, or 6 hours when it is off). The chosen ones get SIGTERM, and you get cd <folder> && claude --resume <session id> for each, with a copy action. The conversation itself stays on disk.claudeMultiAccount.idleSessions.autoStopHours to a number of hours (default 0, off). Every 30 minutes, sessions idle longer than that are stopped, and a notification lists them with their resume commands.A session is stopped only when all of these hold, checked again right before the signal:
idle. A busy session is never stopped, not even when you pick it.It covers the accounts Billable knows (the accounts file, or ~/.claude), on macOS and Linux.
Claude Code keeps conversations, memory and settings in the account's config directory, so switching a folder to another account starts it without the memory it built up. Billable: Share conversations, memory and settings with the default account (or Change on the sharing line of the hover) offers three levels per account:
projects (transcripts and each project's memory), file-history, todos, history.jsonl and your CLAUDE.md.settings.json, keybindings.json, plugins, commands, agents, skills and output-styles.Each shared item becomes a link to the default account's copy. What the account had is merged in where the default account lacks it, and the rest is moved to a billable-backup-<time> folder in the account's directory. .claude.json and the keychain entry are never shared. Sharing less copies the settings back, and leaves the conversations with the default account. The choice is kept in ~/.claude-multi-account/sync and applied again when VS Code starts, in case Claude Code replaced a link with a file of its own.
Pro adds the full history of the cost report, beyond the current and the previous month, and CSV export for invoices. Every Pro feature is free during the launch. When Pro goes on sale, Billable: Enter Pro license key unlocks it on your machine.
/plugin install multi-account --marketplace roych28/claude-multi-account ` or from a shell: ` claude plugin install multi-account --marketplace roych28/claude-multi-account ``claudeCode.claudeProcessWrapper to a small launcher in ~/.claude-multi-account/bin.Every Claude account lives in its own config directory, which Claude Code reads from CLAUDE_CONFIG_DIR. Two plain-text files in ~/.claude-multi-account hold the setup:
accounts: one account per line, name<TAB>config dirfolders: one rule per line, folder<TAB>account. The longest matching folder wins, and a * line is the default.sync: one line per account that shares with the default one, account<TAB>memory|all.The launcher reads these files and starts Claude Code on the right account. The companion plugin reports the account, cost and limits of each session, records each session's cost in ~/.claude-multi-account/ledger, and writes the handoff note the next session reads. The cost is Claude Code's own figure at API prices; on a Pro or Max plan it measures the work done, not what the plan charged.
The same rules work for claude in any terminal. Add this to ~/.zshrc or ~/.bashrc:
claude() { ~/.claude-multi-account/bin/cma exec claude "$@"; }
On Windows, add this to your PowerShell profile (notepad $PROFILE):
function claude { & "$HOME\.claude-multi-account\bin\cma.exe" exec claude.exe @args }
Inside a session, /account shows the account, cost and limits, and /account <name> switches the folder. The plugin also draws the account and its switch buttons above the prompt.
cd vscode && npm test: unit tests for the rules, tokens, cost report and license.cd vscode && npm run test:integration: runs the extension inside a real VS Code against a throwaway setup.claude plugin test ./plugin: the Claude Code plugin's tests.scripts/check-accounts: a live check on the accounts of this machine. For each signed-in account it runs one short session through the launcher and checks that it ran on that account, was reported by the plugin and landed in the cost ledger. Then it moves a folder between two accounts and checks the second one picks up where the first stopped. It uses a temporary setup, never your own rules, and costs a few cents per account.plugin/: the Claude Code plugin multi-accountvscode/: the VS Code extension, with the launcher in vscode/binjetbrains/: the same extension for IntelliJ IDEA, PyCharm, WebStorm and the other JetBrains IDEs (./gradlew buildPlugin)Billable: Switch account for this folderBillable: Cost report by accountBillable: Add accountBillable: Sign in to an account againBillable: Remove accountBillable: Share conversations, memory and settings with the default accountBillable: Stop idle sessionsBillable: Start Claude Code tabs on the folder's accountBillable: RefreshBillable: Enter Pro license keyclaudeMultiAccount.idleSessions.autoStopHours: stop sessions idle longer than this many hours, checked every 30 minutes. 0 (the default) turns it off; any other value counts as at least 1 hour.macOS, Linux or Windows, with Claude Code installed. The launcher is a shell script on macOS and Linux, and a small .exe on Windows (built from launcher/, Go), because the Claude Code extension starts its wrapper without a shell there.
Everything stays on your machine. The extension reads Claude Code's local config, session files and transcripts, lists processes with ps, and writes only to ~/.claude-multi-account. It signals a Claude Code process only when you stop a session, or when you turn on automatic stopping. The one network call is to the license server, with your key, when you enter a Pro license key.
Found a bug or want a feature? Open an issue. If the extension saves you time, you can sponsor its development.
MIT. Not affiliated with Anthropic. Made by Roy Chemo at Kashuv.
hooks/register.tsx 474 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, Timer } from 'claude-code'
3
4import type { ClaudeMultiAccountAccount, ClaudeMultiAccountInfo, ClaudeMultiAccountUsage } from '../types'
5
6const info = atom({ plugin: 'multi-account', key: 'info' } as const, null)
7const usage = atom({ plugin: 'multi-account', key: 'usage' } as const, { limits: [] })
8const nextAccount = atom({ plugin: 'multi-account', key: 'next' } as const, null)
9const handoff = atom({ plugin: 'multi-account', key: 'handoff' } as const, null)
10const run = atom({ plugin: 'multi-account', key: 'run' } as const, null)
11
12const HANDOFF_MAX_AGE_MS = 3 * 24 * 60 * 60 * 1000
13// How often the folder's rule and the other accounts are read again: both change outside this session.
14const REFRESH_MS = 5_000
15const NOT_SIGNED_IN = 'not signed in'
16const LIMIT_NAMES: Record<string, string> = { five_hour: '5-hour limit', seven_day: 'Weekly limit' }
17
18type Handoff = {
19 sessionId: string
20 account: string
21 at: number
22 transcript: string
23 lastRequest: string
24 lastAnswer: string
25 summary?: string
26}
27
28type Rule = { path: string; account: string }
29
30async function home($: EngineInterface): Promise<string> {
31 return (await $.env.get('HOME')) || (await $.env.get('USERPROFILE')) || ''
32}
33
34async function base($: EngineInterface): Promise<string> {
35 return (await $.env.get('CLAUDE_MULTI_ACCOUNT_HOME')) || `${await home($)}/.claude-multi-account`
36}
37
38function expand(p: string, h: string): string {
39 return (p === '~' || p.startsWith('~/') ? h + p.slice(1) : p).replace(/\/+$/, '')
40}
41
42async function readLines($: EngineInterface, path: string): Promise<string[][]> {
43 try {
44 return (await $.fs.read(path))
45 .split('\n')
46 .map(l => l.trim())
47 .filter(l => l !== '' && !l.startsWith('#'))
48 .map(l => l.split('\t').map(f => f.trim()))
49 } catch {
50 return []
51 }
52}
53
54async function emailOf($: EngineInterface, dir: string, h: string): Promise<string> {
55 const file = pathKey(dir) === pathKey(`${h}/.claude`) ? `${h}/.claude.json` : `${dir}/.claude.json`
56 try {
57 const parsed = JSON.parse(await $.fs.read(file)) as { oauthAccount?: { emailAddress?: string } }
58 return parsed.oauthAccount?.emailAddress ?? NOT_SIGNED_IN
59 } catch {
60 return NOT_SIGNED_IN
61 }
62}
63
64async function accounts($: EngineInterface): Promise<ClaudeMultiAccountAccount[]> {
65 const h = await home($)
66 const rows = await readLines($, `${await base($)}/accounts`)
67 const list = rows.filter(r => r[0] && r[1]).map(r => ({ name: r[0]!, dir: expand(r[1]!, h) }))
68 const all = list.length > 0 ? list : [{ name: 'default', dir: `${h}/.claude` }]
69 const out: ClaudeMultiAccountAccount[] = []
70 for (const a of all) out.push({ ...a, email: await emailOf($, a.dir, h) })
71 return out
72}
73
74async function rules($: EngineInterface): Promise<Rule[]> {
75 const h = await home($)
76 const rows = await readLines($, `${await base($)}/folders`)
77 return rows.filter(r => r[0] && r[1]).map(r => ({ path: r[0] === '*' ? '*' : expand(r[0]!, h), account: r[1]! }))
78}
79
80// How two paths are compared: forward slashes, no trailing slash, and case-insensitive for Windows drive paths.
81export function pathKey(p: string): string {
82 const k = p.replace(/\\/g, '/').replace(/\/+$/, '')
83 return /^[a-zA-Z]:\//.test(k) ? k.toLowerCase() : k
84}
85
86export function accountFor(folder: string, list: Rule[]): string | null {
87 const f = pathKey(folder)
88 let best: Rule | null = null
89 let bestLen = -1
90 for (const r of list) {
91 if (r.path === '*') {
92 if (bestLen < 0) {
93 best = r
94 bestLen = 0
95 }
96 continue
97 }
98 const p = pathKey(r.path)
99 if ((f === p || f.startsWith(`${p}/`)) && p.length > bestLen) {
100 best = r
101 bestLen = p.length
102 }
103 }
104 return best?.account ?? null
105}
106
107// The folder a switch pins: the repository root, unless a rule deeper than it already covers this session's folder
108// (the VS Code status bar pins the workspace folder, which can sit inside a repository). Pinning the root then
109// would change nothing, since the longest matching rule wins.
110export function pinTarget(root: string, cwd: string, list: Rule[]): string {
111 const r = pathKey(root)
112 const c = pathKey(cwd)
113 let best = root
114 for (const rule of list) {
115 if (rule.path === '*') continue
116 const p = pathKey(rule.path)
117 const coversCwd = c === p || c.startsWith(`${p}/`)
118 if (coversCwd && p.startsWith(`${r}/`) && p.length > pathKey(best).length) best = rule.path
119 }
120 return best
121}
122
123async function folderRoot($: EngineInterface, cwd: string): Promise<string> {
124 try {
125 const ran = await $.process.run(['git', 'rev-parse', '--show-toplevel'], { cwd })
126 return ran.exitCode === 0 && ran.stdout.trim() ? ran.stdout.trim() : cwd
127 } catch {
128 return cwd
129 }
130}
131
132function keyOf(path: string): string {
133 return path.replace(/[^a-zA-Z0-9]/g, '-')
134}
135
136async function loadInfo($: EngineInterface, cwd: string): Promise<ClaudeMultiAccountInfo> {
137 const h = await home($)
138 const list = await accounts($)
139 const dir = expand((await $.env.get('CLAUDE_CONFIG_DIR')) || `${h}/.claude`, h)
140 const current = list.find(a => pathKey(a.dir) === pathKey(dir)) ?? {
141 name: dir.split('/').pop() ?? dir,
142 dir,
143 email: await emailOf($, dir, h),
144 }
145 const sessionId = await $.session.id()
146 return {
147 current,
148 others: list.filter(a => pathKey(a.dir) !== pathKey(current.dir) && a.email !== NOT_SIGNED_IN),
149 folder: await folderRoot($, cwd),
150 transcript: `${dir}/projects/${keyOf(cwd)}/${sessionId}.jsonl`,
151 }
152}
153
154function clip(text: string, max: number): string {
155 return text.length > max ? `${text.slice(0, max)} [...]` : text
156}
157
158async function handoffPath($: EngineInterface, folder: string): Promise<string> {
159 return `${await base($)}/handoff/${keyOf(folder)}.json`
160}
161
162async function readHandoff($: EngineInterface, folder: string): Promise<Handoff | null> {
163 try {
164 return JSON.parse(await $.fs.read(await handoffPath($, folder))) as Handoff
165 } catch {
166 return null
167 }
168}
169
170async function writeHandoff($: EngineInterface, summary?: string): Promise<void> {
171 const i = await read($, info)
172 if (!i) return
173 const sessionId = await $.session.id()
174 const messages = await $.session.messages()
175 const lastRequest = [...messages].reverse().find(m => m.role === 'user' && m.text.trim() !== '')
176 const lastAnswer = [...messages].reverse().find(m => m.role === 'assistant' && m.text.trim() !== '')
177 const previous = await readHandoff($, i.folder)
178 const keptSummary = summary ?? (previous?.sessionId === sessionId ? previous.summary : undefined)
179 const note: Handoff = {
180 sessionId,
181 account: i.current.name,
182 at: await $.clock.now(),
183 transcript: i.transcript,
184 lastRequest: clip(lastRequest?.text ?? '', 2000),
185 lastAnswer: clip(lastAnswer?.text ?? '', 3000),
186 ...(keptSummary ? { summary: keptSummary } : {}),
187 }
188 await $.fs.write(await handoffPath($, i.folder), JSON.stringify(note, null, 2))
189}
190
191async function writeStatus($: EngineInterface): Promise<void> {
192 const i = await read($, info)
193 if (!i) return
194 const sessionId = await $.session.id()
195 const status = {
196 sessionId,
197 cwd: await $.session.cwd(),
198 folder: i.folder,
199 transcript: i.transcript,
200 account: { name: i.current.name, email: i.current.email },
201 others: i.others.map(o => ({ name: o.name, email: o.email })),
202 usage: await read($, usage),
203 next: await read($, nextAccount),
204 updatedAt: await $.clock.now(),
205 }
206 await $.fs.write(`${await base($)}/sessions/${sessionId}.json`, JSON.stringify(status))
207 await writeLedger($, sessionId, i, status.usage, status.updatedAt)
208}
209
210// One ledger entry per run of a session, so a session resumed on another account is billed to each account
211// for its own part. The cost counts from the run's start, whether or not Claude Code restores it on resume.
212async function writeLedger(
213 $: EngineInterface,
214 sessionId: string,
215 i: ClaudeMultiAccountInfo,
216 u: ClaudeMultiAccountUsage,
217 now: number,
218): Promise<void> {
219 const r = await read($, run)
220 if (!r || u.usd === undefined) return
221 const entry = {
222 sessionId,
223 account: i.current.name,
224 email: i.current.email,
225 folder: i.folder,
226 startedAt: r.startedAt,
227 updatedAt: now,
228 usd: Math.max(0, u.usd - r.startUsd),
229 }
230 await $.fs.write(`${await base($)}/ledger/${sessionId}-${r.startedAt}.json`, JSON.stringify(entry))
231}
232
233function handoffPrompt(note: Handoff): string {
234 return [
235 '# Where the previous session in this folder stopped',
236 `Session ${note.sessionId} on the ${note.account} account, last active ${new Date(note.at).toISOString()}.`,
237 'If the person continues earlier work, pick up from here without asking them to repeat it; if they start something new, ignore this.',
238 `Full transcript (JSONL, read its tail for more detail): ${note.transcript}`,
239 '',
240 'Last request from the person:',
241 note.lastRequest || '(none)',
242 '',
243 'Last answer:',
244 note.lastAnswer || '(none)',
245 ...(note.summary ? ['', 'Handoff summary written when the account was switched:', note.summary] : []),
246 ].join('\n')
247}
248
249const SUMMARY_PROMPT =
250 'The person is moving this work to another Claude account; a fresh session will continue from a note. ' +
251 'Write that handoff note in the language of the conversation, under 300 words: the goal, what is done ' +
252 '(with full file paths and commits), what is in progress, the exact next step, and open questions. ' +
253 'No secrets, keys or tokens. Reply with the note only.'
254
255async function pin($: EngineInterface, folder: string, name: string): Promise<void> {
256 const path = `${await base($)}/folders`
257 let lines: string[] = []
258 try {
259 lines = (await $.fs.read(path)).split('\n').filter(l => l.trim() !== '')
260 } catch {}
261 const h = await home($)
262 const kept = lines.filter(l => {
263 const first = l.split('\t')[0]?.trim() ?? ''
264 return l.trim().startsWith('#') || first === '*' || pathKey(expand(first, h)) !== pathKey(folder)
265 })
266 if (name !== 'auto') kept.push(`${folder}\t${name}`)
267 await $.fs.write(path, kept.join('\n') + '\n')
268}
269
270async function switchTo($: EngineInterface, name: string): Promise<string> {
271 const i = await read($, info)
272 if (!i) return 'Not ready yet.'
273 const list = await accounts($)
274 if (name !== 'auto' && !list.some(a => a.name === name)) {
275 return `Unknown account: ${name}. Known: ${list.map(a => a.name).join(', ')}, or auto.`
276 }
277 // The launcher picks the account from the session's own folder, so that is what the switch has to reach.
278 const cwd = await $.session.cwd()
279 const target = pinTarget(i.folder, cwd, await rules($))
280 await pin($, target, name)
281 const next = accountFor(cwd, await rules($))
282 await update($, nextAccount, () => next)
283
284 const fork = await $.model.fork({ prompt: SUMMARY_PROMPT })
285 let note = ''
286 if (fork.isAnswered) {
287 await writeHandoff($, fork.text.trim())
288 } else {
289 await writeHandoff($)
290 note = `\nNo handoff summary (${fork.reason}): the next session gets the last request, the last answer and the transcript path.`
291 $.ui.toast(`Handoff summary not written (${fork.reason})`)
292 }
293 await writeStatus($)
294 return (
295 `${target} now runs on ${next ?? 'the default account'}.${note}\n` +
296 'To continue from here: open a new Claude tab, or exit and run claude --continue in this folder.'
297 )
298}
299
300function usageLine(u: ClaudeMultiAccountUsage): string {
301 const cost = `Session cost ${u.usd === undefined ? 'unknown' : `$${u.usd.toFixed(2)}`}`
302 return [cost, ...u.limits.map(l => `${LIMIT_NAMES[l.kind] ?? l.kind} ${l.percentUsed}%`)].join(' · ')
303}
304
305function statusText(i: ClaudeMultiAccountInfo, u: ClaudeMultiAccountUsage, next: string | null): string {
306 const lines = [
307 `Account: ${i.current.name} (${i.current.email})`,
308 `Other accounts: ${i.others.map(o => `${o.name} (${o.email})`).join(', ') || 'none signed in'}`,
309 usageLine(u),
310 ]
311 if (next && next !== i.current.name) lines.push(`The next session in ${i.folder} runs on ${next}.`)
312 lines.push('Switch with /account <name>, or /account auto for the default.')
313 return lines.join('\n')
314}
315
316// A running session keeps its account. After the folder is switched (from the VS Code status bar, /account or
317// another session) the line says where a new conversation runs, so it never reads as if nothing changed.
318function vscodeStatus(i: ClaudeMultiAccountInfo, next: string | null): string {
319 const line = `Claude account: ${i.current.name} (${i.current.email})`
320 return next && next !== i.current.name ? `${line}. New conversations in this folder run on ${next}.` : line
321}
322
323async function refresh($: EngineInterface): Promise<void> {
324 const i = await read($, info)
325 if (!i) return
326 const list = await accounts($)
327 const current = list.find(a => pathKey(a.dir) === pathKey(i.current.dir)) ?? i.current
328 const others = list.filter(a => pathKey(a.dir) !== pathKey(current.dir) && a.email !== NOT_SIGNED_IN)
329 await update($, info, () => ({ ...i, current, others }))
330 const ruled = accountFor(await $.session.cwd(), await rules($))
331 await update($, nextAccount, () => ruled)
332}
333
334let timer: Timer | undefined
335// The status line as last set, so an unchanged one is not set again every few seconds.
336let shown: string | undefined
337
338async function refreshStatus($: EngineInterface): Promise<void> {
339 await refresh($)
340 const i = await read($, info)
341 if (!i || (await $.env.get('CLAUDE_CODE_ENTRYPOINT')) !== 'claude-vscode') return
342 const text = vscodeStatus(i, await read($, nextAccount))
343 if (text === shown) return
344 shown = text
345 $.ui.status(text)
346}
347
348export const register: Register = on => {
349 on('session.start', async ($, e, next) => {
350 const loaded = await loadInfo($, e.cwd)
351 await update($, info, () => loaded)
352 await update($, nextAccount, () => null)
353 shown = undefined
354 timer?.cancel()
355 timer = $.clock.every(REFRESH_MS, () => {
356 refreshStatus($).catch(() => {})
357 })
358
359 const now = await $.session.usage()
360 await update($, usage, () => ({
361 usd: now.cost?.usd,
362 limits: now.rateLimits.map(l => ({ kind: l.kind, percentUsed: l.percentUsed })),
363 }))
364 const startedAt = await $.clock.now()
365 await update($, run, () => ({ startedAt, startUsd: now.cost?.usd ?? 0 }))
366
367 const names = (await accounts($)).map(a => a.name)
368 await $.command.register({
369 name: 'account',
370 description: 'Show the Claude account, cost and limits; /account <name> switches this folder',
371 argumentHint: `[${[...names, 'auto'].join('|')}]`,
372 })
373
374 const isFresh = (await $.session.messages()).length === 0
375 const note = await readHandoff($, loaded.folder)
376 const sessionId = await $.session.id()
377 const isRecent = note !== null && (await $.clock.now()) - note.at < HANDOFF_MAX_AGE_MS
378 // Only across a switch: on the same account a new conversation is a fresh start, and the old one is a resume away.
379 const isSwitch = note !== null && note.account !== loaded.current.name
380 if (isFresh && note && isRecent && isSwitch && note.sessionId !== sessionId) {
381 await update($, handoff, () => handoffPrompt(note))
382 }
383 await writeStatus($)
384
385 if ((await $.env.get('CLAUDE_CODE_ENTRYPOINT')) === 'claude-vscode') {
386 const text = `Claude account: ${loaded.current.name} (${loaded.current.email})`
387 shown = text
388 $.ui.status(text)
389 try {
390 await $.session.append({ message: { type: 'system', content: [{ type: 'text', text }] } })
391 } catch (err) {
392 $.ui.toast(`${text}. The notice could not be added: ${String(err)}`)
393 }
394 }
395
396 return next(e)
397 })
398
399 on('prompt.submit', async ($, e, next) => {
400 if (e.command || e.isMeta) return next(e)
401 const text = await read($, handoff)
402 if (!text) return next(e)
403 await update($, handoff, () => null)
404 return next({ ...e, context: [...(e.context ?? []), text] })
405 })
406
407 on('session.measure', async ($, e, next) => {
408 await update($, usage, () => ({
409 usd: e.cost?.usd,
410 limits: e.rateLimits.map(l => ({ kind: l.kind, percentUsed: l.percentUsed })),
411 }))
412 await writeStatus($)
413 return next(e)
414 })
415
416 on('turn.complete', async ($, e, next) => {
417 const done = await next(e)
418 if (e.agentId === undefined) {
419 try {
420 await writeHandoff($)
421 } catch (err) {
422 $.ui.toast(`Handoff note not saved: ${String(err)}`)
423 }
424 }
425 await writeStatus($)
426 return done
427 })
428
429 on('command.run', { command: 'account' }, async ($, e) => {
430 const name = e.args.trim()
431 if (name) return { text: await switchTo($, name) }
432 const i = (await read($, info)) ?? (await loadInfo($, await $.session.cwd()))
433 return { text: statusText(i, await read($, usage), await read($, nextAccount)) }
434 })
435
436 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
437 if (e.props.hasSurvey) return next(e)
438 if ((await $.env.get('CLAUDE_CODE_ENTRYPOINT')) === 'claude-vscode') return next(e)
439 const i = await read($, info)
440 if (!i) return next(e)
441 const { Box, Text, Button } = $.ui.resolve(e)
442 const u = await read($, usage)
443 const pending = await read($, nextAccount)
444
445 return (
446 <Box flexDirection="column">
447 <Box flexDirection="row">
448 <Text bold>{i.current.name}</Text>
449 <Text dimColor> {i.current.email}</Text>
450 </Box>
451 {i.others.length > 0 && (
452 <Box flexDirection="row">
453 <Text dimColor>Switch to </Text>
454 {i.others.map(o => (
455 <Button
456 key={`switch-${o.name}`}
457 label={o.name}
458 onPress={async () => {
459 const result = await switchTo($, o.name)
460 $.ui.toast(result.split('\n')[0] ?? result)
461 }}
462 />
463 ))}
464 </Box>
465 )}
466 {pending && pending !== i.current.name && (
467 <Text color="yellow">The next session in this folder runs on {pending}</Text>
468 )}
469 <Text dimColor>{usageLine(u)}</Text>
470 </Box>
471 )
472 })
473}
474types/index.d.ts 26 lines1export type ClaudeMultiAccountAccount = { name: string; dir: string; email: string }
2
3export type ClaudeMultiAccountInfo = {
4 current: ClaudeMultiAccountAccount
5 others: ClaudeMultiAccountAccount[]
6 folder: string
7 transcript: string
8}
9
10export type ClaudeMultiAccountUsage = {
11 usd?: number
12 limits: { kind: string; percentUsed: number }[]
13}
14
15declare module 'claude-code' {
16 interface PluginState {
17 'multi-account': {
18 info: ClaudeMultiAccountInfo | null
19 usage: ClaudeMultiAccountUsage
20 next: string | null
21 handoff: string | null
22 run: { startedAt: number; startUsd: number } | null
23 }
24 }
25}
26