Run different Claude accounts side by side: one config directory per account, bound per project, switched with /account.

Repo contains configuration for personal machines, mostly running nix-darwin. I have no idea what I'm doing, and the deeper I go the less of a clue I have apparently.
Originally forked from ironicbadger/nix-config; now maintained independently.
hooks/register.ts 59 lines1import type { EngineInterface, Register } from 'claude-code'
2
3const COMMAND = 'account'
4const PASSED_ON = new Set(['list', 'status', 'add', 'use'])
5
6export const register: Register = on => {
7 on('session.start', async ($, e, next) => {
8 await $.command.register({
9 name: COMMAND,
10 description: 'Show Claude accounts, or bind this project to one',
11 argumentHint: '[<name> | default | add <name> | status]',
12 immediate: true,
13 })
14 const active = await run($, ['active'])
15 if (active.isOk) $.ui.status(`account: ${active.text}`)
16
17 return next(e)
18 })
19
20 on('command.run', { command: COMMAND }, async ($, e) => {
21 const words = e.args.trim().split(/\s+/).filter(Boolean)
22 const [first = 'list'] = words
23 const argv = PASSED_ON.has(first) ? words.length ? words : ['list'] : ['use', first]
24 const ran = await run($, argv)
25 if (!ran.isOk || argv[0] !== 'use') return { text: ran.text }
26
27 const active = await run($, ['active'])
28 if (!active.isOk || active.text === argv[1]) {
29 if (active.isOk) $.ui.status(`account: ${active.text}`)
30 return { text: ran.text }
31 }
32
33 // Ends this session's claude; the launcher resumes it under the new account.
34 $.ui.status(`account: switching to ${argv[1]}`)
35 const switched = await run($, ['switch', argv[1], await $.session.id()])
36 if (switched.isOk) return { text: `${ran.text}\nSwitching this session to '${argv[1]}'…` }
37
38 $.ui.status(`account: ${active.text} (${argv[1]} after restart)`)
39 return {
40 text: `${ran.text}\nThis session stays on '${active.text}': ${switched.text.replace(/^claude-account: /, '')}`,
41 }
42 })
43}
44
45type Ran = { isOk: boolean; text: string }
46
47async function run($: EngineInterface, argv: string[]): Promise<Ran> {
48 try {
49 const { exitCode, stdout, stderr } = await $.process.run([
50 `${$.plugin.root}/bin/claude-account`,
51 ...argv,
52 ])
53
54 return { isOk: exitCode === 0, text: (exitCode === 0 ? stdout : stderr || stdout).trim() }
55 } catch (error) {
56 return { isOk: false, text: `claude-account did not run: ${String(error)}` }
57 }
58}
59