claudehop-cli 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,174 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+ This project follows [semantic versioning](https://semver.org/).
5
+
6
+ ## [1.4.0] — 2026-08-11
7
+
8
+ ### Added
9
+
10
+ - **On PyPI.** `claudehop` installs from the index and puts both `claudehop` and
11
+ `hop` on your PATH:
12
+
13
+ ```bash
14
+ pipx install claudehop
15
+ ```
16
+
17
+ Still no dependencies, still one file you can read in a sitting. `install.sh`
18
+ is unchanged and remains the way to get the shell glue with it.
19
+ - **Tagged releases build and publish themselves.** `.github/workflows/release.yml`
20
+ checks the tag against `VERSION`, builds the wheel and the sdist, publishes to
21
+ PyPI with trusted publishing (no API token anywhere), and attaches both files
22
+ to the GitHub release.
23
+ - **`claudehop shell-init`** prints the shell glue a packaged install needs —
24
+ the `hop` alias and tab-completion, without the PATH line the repo installer
25
+ writes. `eval "$(claudehop shell-init)"` in your rc file.
26
+
27
+ ### Changed
28
+
29
+ - The tool moved from `bin/claudehop` to `claudehop.py` at the top of the repo,
30
+ so it can be both the installed script and the packaged module. `install.sh`,
31
+ `~/.claude/bin/claudehop` and every command name are unaffected.
32
+ - Tab-completion now offers `shell-init` and `version`, and the completion list
33
+ is generated from the command table — a test fails if the sourced glue and the
34
+ packaged one drift apart.
35
+
36
+ ## [1.3.0] — 2026-08-06
37
+
38
+ ### Added
39
+
40
+ - **`doctor` now works out when you next have to log in for real, and the
41
+ cheapest day to do it.** A refresh token lasts about 30 days from the `/login`
42
+ that issued it and using the account does not extend it — measured across four
43
+ accounts, one of which had its access token reissued that morning and still
44
+ expired 30 days after its first login. So it is a hard monthly expiry per
45
+ account, and with several accounts the dates drift apart into a browser
46
+ round-trip per account per month. Logging in early resets the full window, so
47
+ `doctor` names the account that goes first and says to do them all that day,
48
+ which collapses them onto one date:
49
+
50
+ ```
51
+ re-login by 2026-08-30 (work); the other 3 by 2026-09-04
52
+ windows are ~30d from login and do not slide, so re-login all 4 on
53
+ 2026-08-30 and they collapse to one date
54
+ ```
55
+
56
+ `doctor --json` carries the same thing under `reloginPlan`, and an account whose
57
+ refresh token has already died is called out separately — that one needs
58
+ `claudehop add` rather than planning.
59
+
60
+ ### Changed
61
+
62
+ - **The expiry warning starts 14 days out instead of 7.** With several accounts on
63
+ staggered dates, one week is not enough notice to plan a single sitting.
64
+
65
+ ## [1.2.1] — 2026-08-06
66
+
67
+ ### Changed
68
+
69
+ - **`hop` on its own now asks which account you want.** It prints the list
70
+ numbered and switches to the one you pick; Enter leaves you where you are. You
71
+ no longer have to read a table and then retype a name out of it. Piped output,
72
+ `--json` and a single saved account all still get the plain listing, so
73
+ statuslines and scripts are unaffected.
74
+ - **The default listing is name, email and plan.** Token expiry and save dates
75
+ moved behind `--long`. `expired (auto-renews)` under `TOKEN` needed a paragraph
76
+ of README to be readable and was the most-misread thing the tool printed; it is
77
+ not something you need in order to choose an account.
78
+ - **`add` stops if other `claude` sessions are running** instead of warning and
79
+ carrying on into a corrupted result. `--yes` overrides it.
80
+ - **`add` tells you to use a private browser window** before it hands you to
81
+ `/login`, rather than leaving it in the README's gotchas.
82
+ - `help` leads with the three commands you actually use and lists the rest below.
83
+
84
+ ### Fixed
85
+
86
+ - **`add` could lose a login it had already completed.** The new credential was
87
+ read inside the cleanup path but written to disk after it, with an API call for
88
+ the account's email in between. Anything that ended the process in that window
89
+ — Ctrl-C, closing the terminal, the identity lookup being cut short — left you
90
+ logged in as an account with no profile saved for it. The token is now written
91
+ first and the email filled in afterwards, so an interrupted `add` still leaves
92
+ a usable profile. Recover one from an older version with `claudehop save <name>`
93
+ while that login is still live.
94
+ - **`add` no longer unwinds on the first Ctrl-C.** In Claude Code, Ctrl-C cancels
95
+ the current turn rather than quitting, so `add` used to start cleaning up while
96
+ `claude` was still running and still writing to the credential store. It now
97
+ ignores SIGINT while the child runs, the way a shell does, and lets `claude`
98
+ decide when to exit. SIGHUP and SIGTERM are turned into a clean unwind instead
99
+ of killing the process outright.
100
+ - **`add` no longer files one account's credential under another account's
101
+ name.** A `claude` session that was already running writes its own refreshed
102
+ token into the credential store, and `add` would save that as the new account.
103
+ It now checks the account UUID before keeping the profile, and puts a refreshed
104
+ token back where it belongs instead.
105
+ - **Replacing an existing account no longer keeps the old email and plan** next
106
+ to the new account's token.
107
+
108
+ ## [1.2.0] — 2026-08-05
109
+
110
+ ### Changed
111
+
112
+ - **Renamed to `claudehop`.** The command is `claudehop`, with `hop` as a shell
113
+ alias. `claude-acct` and `cacct` keep working as shell functions, so nothing
114
+ you have typed before breaks.
115
+ - Environment variables are now `CLAUDE_HOP_BACKEND`, `CLAUDE_HOP_OFFLINE` and
116
+ `CLAUDE_HOP_TIMEOUT`. The old `CLAUDE_ACCT_*` names are still honoured.
117
+ `CLAUDE_CONFIG_DIR` and `CLAUDE_ACCOUNTS_DIR` are unchanged.
118
+
119
+ Your saved accounts do not move: they stay in `~/.claude/accounts/`, and
120
+ `install.sh` removes the old `claude-acct` binary and glue so you never end up
121
+ with two copies on your PATH.
122
+
123
+ ## [1.1.0] — 2026-08-05
124
+
125
+ First public release, under the name `claude-acct`.
126
+
127
+ ### Added
128
+
129
+ - **macOS keychain backend.** Reads and writes the `Claude Code-credentials`
130
+ item the same way Claude Code does, and falls back to `.credentials.json`
131
+ when the keychain holds no login. Override with `CLAUDE_ACCT_BACKEND`.
132
+ - **`doctor`** — checks permissions, stale files, dangling pointers, expiring
133
+ refresh tokens, environment overrides and running sessions. `--fix` repairs
134
+ what it safely can.
135
+ - **`active`** — prints just the active account name with no network call, for
136
+ statuslines and scripts.
137
+ - **`--json`** on `list`, `whoami`, `active` and `doctor`. No secrets in it.
138
+ - **A lock** (`accounts/.lock`) around every mutating command, so two switches
139
+ at once can't interleave and lose an account.
140
+ - Warning when an account's *refresh* token is within a week of expiring —
141
+ that's the one whose death means a real re-login.
142
+ - `--version`, `--no-color`, `NO_COLOR` and `CLICOLOR_FORCE` support.
143
+ - zsh tab-completion (via `bashcompinit`), and `--no-rc` for `install.sh`.
144
+
145
+ ### Fixed
146
+
147
+ - **`add` could leave you logged out of everything.** If `claude` was missing,
148
+ exited without a login, or you hit Ctrl-C, the credentials it had cleared were
149
+ never put back. They are now restored in a `finally` block.
150
+ - **Crash on macOS.** The running-session check read `/proc` unconditionally;
151
+ it now falls back to `pgrep` and never raises.
152
+ - **Misaligned table.** Colour escapes inside a cell were counted as visible
153
+ width, so one expired token knocked every later column out of line.
154
+ - **`list --verify` reported healthy accounts as `invalid`.** An access token
155
+ that has simply aged out now reads `stale (renews)`, which is what actually
156
+ happens to it.
157
+ - `rename` leaked a file descriptor and could leave the active pointer behind.
158
+ - `rm` left the active pointer naming a deleted account.
159
+ - Unknown options (`--typo`) were silently treated as an account name; they now
160
+ exit 2 with a usage error.
161
+ - Account names are validated, so a name can't escape the accounts directory,
162
+ and an auto-generated name can no longer shadow a command.
163
+ - `list --verify` only rewrites a profile when something actually changed.
164
+ - `claude-acct list | head` no longer prints a `BrokenPipeError` traceback.
165
+
166
+ ### Changed
167
+
168
+ - `list` no longer calls the API on the default path — the bare command is now
169
+ local-only and instant. `--verify` still asks, and now asks for every account
170
+ in parallel.
171
+ - Profile writes no longer leave `<name>.json.bak` files lying around; they were
172
+ extra copies of a live credential. `doctor --fix` removes old ones.
173
+ - The `STATE` column only appears with `--verify`, where it means something.
174
+ - `whoami` exits 1 when nothing is logged in.
@@ -0,0 +1,53 @@
1
+ # Contributing
2
+
3
+ Thanks for looking. This is a small tool and it intends to stay small.
4
+
5
+ ## The rules that keep it small
6
+
7
+ 1. **Python 3.9+, standard library only.** No dependencies, no build step.
8
+ `claudehop.py` must stay a single file you can read in one sitting and run
9
+ straight from a clone — `pyproject.toml` packages that one module and
10
+ nothing else.
11
+ 2. **Never lose a login.** Any code path that replaces the live credential must
12
+ first write what was there into a profile. If you can't identify it, stash it
13
+ under a generated name. Losing a token costs a browser round-trip and,
14
+ sometimes, a device-approval email.
15
+ 3. **Tests use fake credentials and no network.** `test/test-switch.sh` runs
16
+ against a temp `CLAUDE_CONFIG_DIR` with `CLAUDE_ACCT_OFFLINE=1`. A test that
17
+ needs a real account is not a test we can run.
18
+ 4. **Secrets never get printed.** Not in `--json`, not in errors, not in debug
19
+ output.
20
+
21
+ ## Working on it
22
+
23
+ ```bash
24
+ git clone https://github.com/psychofict/claudehop.git
25
+ cd claudehop
26
+ ./install.sh # symlinks, so your edits are live immediately
27
+ ./test/test-switch.sh # 99 checks, about two seconds
28
+ ```
29
+
30
+ Before opening a pull request:
31
+
32
+ ```bash
33
+ ./test/test-switch.sh
34
+ ruff check claudehop.py # if you have it; CI runs it either way
35
+ shellcheck install.sh shell/*.sh test/*.sh
36
+ ```
37
+
38
+ Add a check to `test/test-switch.sh` for anything you fix or add. If the
39
+ behaviour is user-visible, update `README.md` and `CHANGELOG.md` in the same
40
+ pull request.
41
+
42
+ ## Things worth doing
43
+
44
+ - Verification of the macOS keychain backend against a real Mac. It is written
45
+ against Claude Code's own `security` calls and tested through a stand-in, but
46
+ nobody has run it on real hardware yet.
47
+ - fish shell completion (`shell/claudehop.sh` is POSIX/bash + zsh today).
48
+ - A `--verify` cache so `list --verify` doesn't re-ask the API every time.
49
+
50
+ ## Style
51
+
52
+ Match what's there: plain names, comments that explain *why*, error messages
53
+ that tell the user what to do next. Keep line length near 100.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ebenworks
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # The wheel is one module. The sdist also carries the repo installer, the shell
2
+ # glue it wires up, the statusline extra and the test suite.
3
+ include LICENSE CHANGELOG.md CONTRIBUTING.md SECURITY.md install.sh
4
+ include shell/claudehop.sh
5
+ include extras/statusline-snippet.sh
6
+ include test/test-switch.sh
@@ -0,0 +1,293 @@
1
+ Metadata-Version: 2.4
2
+ Name: claudehop-cli
3
+ Version: 1.4.0
4
+ Summary: Hop Claude Code between several Claude accounts without logging in again — swap the saved OAuth login in one keystroke, keep every account's usage pool separate.
5
+ Author-email: Ebenworks <hello@ebenworks.co>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/psychofict/claudehop
8
+ Project-URL: Repository, https://github.com/psychofict/claudehop
9
+ Project-URL: Changelog, https://github.com/psychofict/claudehop/blob/master/CHANGELOG.md
10
+ Project-URL: Issues, https://github.com/psychofict/claudehop/issues
11
+ Project-URL: Author, https://ebenworks.co/
12
+ Keywords: claude,claude-code,anthropic,account-switcher,multi-account,oauth,credentials,keychain,cli,developer-tools,productivity,shell,terminal,ai,llm
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: MacOS :: MacOS X
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Topic :: Software Development :: Version Control
26
+ Classifier: Topic :: Utilities
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Dynamic: license-file
31
+
32
+ <p align="center">
33
+ <img src="https://raw.githubusercontent.com/psychofict/claudehop/master/assets/cover.png" alt="claudehop — hop Claude Code between accounts" width="560">
34
+ </p>
35
+
36
+ <h1 align="center">claudehop</h1>
37
+
38
+ <p align="center">
39
+ <b>Hop Claude Code between several Claude accounts without logging in again.</b><br>
40
+ Personal Max account in one terminal, work Team seat in another —
41
+ <i>two separate usage pools, one machine, no browser round-trip.</i>
42
+ </p>
43
+
44
+ <p align="center">
45
+ <a href="https://pypi.org/project/claudehop/"><img src="https://img.shields.io/pypi/v/claudehop.svg?color=FC5F00&label=pypi" alt="PyPI"></a>
46
+ <a href="https://github.com/psychofict/claudehop/actions/workflows/ci.yml"><img src="https://github.com/psychofict/claudehop/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
47
+ <a href="https://github.com/psychofict/claudehop/blob/master/LICENSE"><img src="https://img.shields.io/badge/licence-MIT-FC5F00.svg" alt="Licence: MIT"></a>
48
+ <img src="https://img.shields.io/badge/python-3.9%2B-1D1009.svg" alt="Python 3.9+">
49
+ <img src="https://img.shields.io/badge/dependencies-none-1D1009.svg" alt="No dependencies">
50
+ </p>
51
+
52
+ <p align="center">
53
+ <a href="#install">Install</a> ·
54
+ <a href="https://github.com/psychofict/claudehop/blob/master/CHANGELOG.md">Changelog</a> ·
55
+ <a href="https://github.com/psychofict/claudehop/blob/master/SECURITY.md">Security</a>
56
+ </p>
57
+
58
+ ---
59
+
60
+ ## Switching accounts
61
+
62
+ Run `hop`, press a number.
63
+
64
+ ```
65
+ $ hop
66
+ NAME EMAIL PLAN
67
+ 1. home me@gmail.com max
68
+ 2. * work me@company.com team
69
+
70
+ hop to which? [1-2, Enter to stay] 1
71
+ switched to home (me@gmail.com, max)
72
+ ```
73
+
74
+ Then start a new `claude`. That's it — that's the whole tool.
75
+
76
+ If you'd rather not read a menu, `hop home` goes straight there. Sessions you
77
+ already have open keep the account they started with; only new ones pick up the
78
+ change.
79
+
80
+ ## Install
81
+
82
+ ```bash
83
+ pipx install claudehop # or: pip install --user claudehop
84
+ eval "$(claudehop shell-init)" # the `hop` alias + tab-completion
85
+ ```
86
+
87
+ Put that `eval` line in your `~/.bashrc` or `~/.zshrc` and open a new terminal.
88
+ `hop` and `claudehop` are the same command.
89
+
90
+ Or from a clone, if you'd rather have it symlinked into `~/.claude` with the
91
+ shell glue written for you:
92
+
93
+ ```bash
94
+ git clone https://github.com/psychofict/claudehop.git
95
+ cd claudehop
96
+ ./install.sh # symlinks into ~/.claude, adds one line to your rc file
97
+ ```
98
+
99
+ `./install.sh --copy` installs copies instead of symlinks, `--no-rc` skips the
100
+ shell wiring, `--uninstall` reverses it. None of them ever touch
101
+ `~/.claude/accounts/`, where the credentials live.
102
+
103
+ Requires Python 3.9+ and Claude Code. Linux and macOS. One module, no
104
+ dependencies.
105
+
106
+ ## Adding an account
107
+
108
+ ```bash
109
+ hop add work
110
+ ```
111
+
112
+ Quit your other `claude` sessions first — `add` will stop and tell you if you
113
+ haven't, because a session left running can write its own token back into the
114
+ credential store mid-login and you'd end up with the wrong account saved under
115
+ that name.
116
+
117
+ `add` then starts `claude` with no login so you can `/login`, and saves whatever
118
+ that produces when you exit with `/exit`. **Paste the login URL into a private
119
+ browser window.** Your normal browser is already signed in as one of your other
120
+ accounts and will authorise that one without asking.
121
+
122
+ If the login produces nothing — you changed your mind, you hit Ctrl-C — your
123
+ previous credentials come back. If it does produce a login, that login is saved
124
+ even if the terminal dies on the way out.
125
+
126
+ Already logged in by hand? `hop save work` names whatever is live right now.
127
+
128
+ ## Everything else
129
+
130
+ ```bash
131
+ hop whoami # who am I right now (asks the API)
132
+ hop --long # add token expiry and save dates to the listing
133
+ hop list --verify # check every saved token against the API
134
+ hop rm <name> # delete a saved account (does not log you out)
135
+ hop rename <a> <b>
136
+ hop doctor # check the setup; --fix repairs what it can
137
+ hop shell-init # shell glue for a pip install: alias + tab-completion
138
+ ```
139
+
140
+ `--json` on `list`, `whoami`, `active` and `doctor` gives machine-readable
141
+ output with no secrets in it, for scripts and statuslines. `hop active` prints
142
+ just the active name with no network call. `hop use <name>` is the long spelling
143
+ of `hop <name>`, and `hop sync` writes the live login back to its own file.
144
+
145
+ ## How it works
146
+
147
+ Claude Code keeps the live login under the `claudeAiOauth` key of its credential
148
+ store. This tool keeps one saved copy of that block per account in
149
+ `~/.claude/accounts/<name>.json` and swaps the active one in and out. The
150
+ `mcpOAuth` key in the same store — your Vercel/Neon/etc. MCP logins — is left
151
+ alone, so switching accounts doesn't sign you out of anything else.
152
+
153
+ | | credential store |
154
+ |---|---|
155
+ | Linux | `~/.claude/.credentials.json` |
156
+ | macOS | login keychain item `Claude Code-credentials`, falling back to the file |
157
+
158
+ The backend is detected from whichever one currently holds a login. Force it
159
+ with `CLAUDE_HOP_BACKEND=file` or `=keychain` if you need to.
160
+
161
+ Nothing else needs patching. Account identity in `~/.claude.json`
162
+ (`oauthAccount`) is re-fetched from the API by Claude Code at startup: put a
163
+ bogus email in there, start a session, and it comes back corrected. So swapping
164
+ the credential is the whole job.
165
+
166
+ Identity, plan and token checks come from `GET /api/oauth/profile` with the
167
+ account's own bearer token. Set `CLAUDE_HOP_OFFLINE=1` to skip every API call.
168
+
169
+ ### Two details that make or break it
170
+
171
+ **Access tokens rotate.** Claude Code refreshes them every few hours and writes
172
+ the new one straight into the credential store. A switcher that identifies the
173
+ active profile by comparing token values therefore stops recognising it after
174
+ the first refresh — and then loses the refreshed token when you switch away. So
175
+ the active profile is tracked in `accounts/active` and confirmed against the
176
+ account UUID from the API, and every switch writes the live block back to its
177
+ profile before loading the next one.
178
+
179
+ **A login you can't identify is stashed, never dropped.** If the live
180
+ credentials match no saved profile (you ran `/login` by hand, say), switching
181
+ saves them under a name derived from the account's email first. You can always
182
+ get back to a session you'd otherwise have to re-authenticate.
183
+
184
+ ## Gotchas
185
+
186
+ - Switching affects **new** `claude` processes. Sessions already running keep the
187
+ account they started with, and will rewrite the credential store when their
188
+ token refreshes — which can silently undo a switch. `claudehop` prints the
189
+ PIDs it finds; quit them for a clean switch.
190
+ - `/login` opens your default browser, which is already signed in as somebody.
191
+ Paste the URL into an incognito window to authenticate as a different account.
192
+ - `ANTHROPIC_API_KEY` and `CLAUDE_CODE_OAUTH_TOKEN` in the environment override
193
+ the saved login entirely. `whoami` and `doctor` warn when either is set.
194
+ - `--long` showing `expired (auto-renews)` under `TOKEN` is normal — the access
195
+ token is short lived and Claude Code renews it from the refresh token.
196
+ `list --verify` prints `stale (renews)` for the same reason. What actually
197
+ matters is the refresh token; see below. This is why the default listing
198
+ doesn't show either of them.
199
+ - Each account still has its own rate limits and its own terms. This moves your
200
+ own logins between your own terminals; it is not a way to pool quota.
201
+
202
+ ## Every account needs a real login about once a month
203
+
204
+ The refresh token is good for roughly 30 days from the `/login` that issued it,
205
+ and **using the account does not extend it.** Measured 2026-08-06 across four
206
+ accounts: one had its access token reissued that morning and its refresh window
207
+ still ended 30 days after its first login, not 30 days after the refresh. So this
208
+ is a hard monthly expiry per account, and nothing on this side can lengthen it —
209
+ it's set by the OAuth server. `claude setup-token` is not a way around it either;
210
+ those tokens expire too, and carry inference scope only.
211
+
212
+ With several accounts the dates drift apart and you get a browser round-trip per
213
+ account per month. Logging in early resets the whole 30 days, so the cheap move is
214
+ to do them all on the day the earliest one comes due — after that they share one
215
+ date and it's one sitting a month. `doctor` works this out for you:
216
+
217
+ ```
218
+ $ hop doctor
219
+ accounts /home/you/.claude/accounts (4 saved)
220
+ active work
221
+ re-login by 2026-08-30 (work); the other 3 by 2026-09-04
222
+ windows are ~30d from login and do not slide, so re-login all 4 on
223
+ 2026-08-30 and they collapse to one date
224
+ ```
225
+
226
+ You also get a per-account warning starting 14 days out, and `doctor --json`
227
+ carries the same thing under `reloginPlan` if you want to hang a reminder off it.
228
+
229
+ ## Why not `claude setup-token`
230
+
231
+ The obvious approach is a long-lived token per account exported as
232
+ `CLAUDE_CODE_OAUTH_TOKEN`. It doesn't hold up:
233
+
234
+ - those tokens carry inference scope only — `/api/oauth/profile` answers `403 OAuth
235
+ token does not meet scope requirement`, so you can't tell whose token you're
236
+ holding or whether it's still good;
237
+ - they expire, and a dead one looks exactly like a live one until a request fails;
238
+ - it's per-shell, so every terminal has to be primed before `claude` starts.
239
+
240
+ Swapping the real credential block avoids all three and matches what Claude Code
241
+ already does to itself.
242
+
243
+ ## Security
244
+
245
+ Saved credentials are real, live Claude logins. `accounts/` is `700`, every
246
+ profile is `600`, and writes are atomic. Nothing is ever sent anywhere except
247
+ `api.anthropic.com` to resolve an email and plan. See
248
+ [SECURITY.md](https://github.com/psychofict/claudehop/blob/master/SECURITY.md)
249
+ for the threat model and how to report a problem.
250
+
251
+ ## Files
252
+
253
+ ```
254
+ claudehop.py the tool (python3, stdlib only)
255
+ shell/claudehop.sh PATH, tab-completion, back-compat aliases
256
+ extras/statusline-snippet.sh show the active account in the Claude Code statusline
257
+ install.sh symlink/copy into ~/.claude, wire up the rc file
258
+ pyproject.toml packaging: one module, no dependencies, two commands
259
+ test/test-switch.sh 99 checks against a throwaway config dir, no network
260
+ assets/ logo, icon, cover and social preview (svg sources + png)
261
+ ```
262
+
263
+ ## Tests
264
+
265
+ ```bash
266
+ ./test/test-switch.sh
267
+ ```
268
+
269
+ Runs entirely inside a temp dir with fake credentials — it never reads or writes
270
+ a real account, and never touches the network. Covers the swap, mcpOAuth
271
+ preservation, token rotation, the stash path, concurrent switches, the macOS
272
+ keychain backend (through a stand-in `security`), `add` rolling back a failed
273
+ login, `add` surviving a teardown after a successful one, the refresh race, JSON
274
+ output, table layout, housekeeping and file permissions.
275
+
276
+ ## Contributing
277
+
278
+ Issues and pull requests welcome — see
279
+ [CONTRIBUTING.md](https://github.com/psychofict/claudehop/blob/master/CONTRIBUTING.md).
280
+ If `claudehop` saved you a browser round-trip this morning, a ⭐ on
281
+ [GitHub](https://github.com/psychofict/claudehop) helps others find it.
282
+
283
+ ## Licence
284
+
285
+ MIT. Not affiliated with, endorsed by, or sponsored by Anthropic. "Claude" and
286
+ "Claude Code" are trademarks of Anthropic, PBC, used here only to say what this
287
+ works with.
288
+
289
+ ---
290
+
291
+ <p align="center">
292
+ Made by <a href="https://ebenworks.co/">Ebenworks</a>
293
+ </p>