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.
- claudehop_cli-1.4.0/CHANGELOG.md +174 -0
- claudehop_cli-1.4.0/CONTRIBUTING.md +53 -0
- claudehop_cli-1.4.0/LICENSE +21 -0
- claudehop_cli-1.4.0/MANIFEST.in +6 -0
- claudehop_cli-1.4.0/PKG-INFO +293 -0
- claudehop_cli-1.4.0/README.md +262 -0
- claudehop_cli-1.4.0/SECURITY.md +41 -0
- claudehop_cli-1.4.0/claudehop.py +1491 -0
- claudehop_cli-1.4.0/claudehop_cli.egg-info/PKG-INFO +293 -0
- claudehop_cli-1.4.0/claudehop_cli.egg-info/SOURCES.txt +17 -0
- claudehop_cli-1.4.0/claudehop_cli.egg-info/dependency_links.txt +1 -0
- claudehop_cli-1.4.0/claudehop_cli.egg-info/entry_points.txt +3 -0
- claudehop_cli-1.4.0/claudehop_cli.egg-info/top_level.txt +1 -0
- claudehop_cli-1.4.0/extras/statusline-snippet.sh +17 -0
- claudehop_cli-1.4.0/install.sh +175 -0
- claudehop_cli-1.4.0/pyproject.toml +62 -0
- claudehop_cli-1.4.0/setup.cfg +4 -0
- claudehop_cli-1.4.0/shell/claudehop.sh +77 -0
- claudehop_cli-1.4.0/test/test-switch.sh +620 -0
|
@@ -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>
|