Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,48 @@ All notable changes to this project are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims to
follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.0] - 2026-07-25

### Added
- Optional hot-switch mode: saved Claude config directories become credential
profiles for one main session directory, with lock-safe Keychain/config swaps,
current-token sync-back, `cctop switch [NAME]`, and the TUI `x` key.
- A headless `cctop autoswitch` supervisor keeps automatic rotation running
independently of the TUI and terminal job control.
- Automatic rotation to the healthy Claude profile with the most headroom at
1% remaining (configurable), including delegated refresh of expired saved
access tokens without a login/logout cycle. Candidates must have strictly
more headroom than the active profile, preventing ping-pong while still using
every account that can make progress.

### Changed
- `get_token` no longer falls back to the default account's Keychain service, so
a logged-out config dir reads as having no token instead of borrowing the
default account's. The default-profile Keychain and identity-path rules now
live in one place (`authctl`) instead of being restated per call site.
- A rejected live credential now triggers delegated refresh and then immediate
profile recovery, even while usage reads are rate-limited. The dead main
credential is never synced over its saved profile, and stale usage data can
no longer select a locally expired target.
- Automatic rotation now accepts an above-threshold profile when it has
strictly more headroom than the active one, so a fully exhausted login can
hand work to a still-usable account without introducing switch ping-pong.
- The TUI and autoswitch supervisor now share last-good usage metadata. During
HTTP 429 backoff, new cctop processes keep showing cached percentages, reset
times, and reading age instead of replacing useful data with an error.
- Saved-profile expiry is checked before the usage request, so a logged-out
account is refreshed or reported as `needs re-login` instead of being
misclassified when the usage endpoint returns a fleet-wide 429 first.
- A recent but partial shared cache no longer suppresses accounts without a
successful reading; every configured profile remains visible.
- A successful fresh profile whose usage windows have not started now ranks as
0% used instead of being excluded from automatic switching as unavailable.

### Preserved
- Original per-config-directory session monitoring and account provisioning
remain the default behavior when `hot_switch` is disabled; the login profiles
cctop snapshots for itself are hidden from account discovery while it is off.

## [0.2.0] - 2026-07-19

### Added
Expand Down Expand Up @@ -52,5 +94,6 @@ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `LICENSE` (MIT), `SECURITY.md`, and packaging metadata (distribution
`cctop-tui`; the command stays `cctop`).

[0.3.0]: https://github.com/ryanirl/cctop/releases/tag/v0.3.0
[0.2.0]: https://github.com/ryanirl/cctop/releases/tag/v0.2.0
[0.1.0]: https://github.com/ryanirl/cctop/releases/tag/v0.1.0
78 changes: 68 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ those up too.

Everything is read from files the tools already write plus a couple of free,
read-only usage reads; nothing here ever spends message quota. cctop touches
your credentials only to make those reads, never logs or transmits a token, and
is read-only except for two explicit, additive account actions. See
[SECURITY.md](SECURITY.md) for the full trust statement.
your credentials only for those reads and optional local hot switching, never
logs or transmits a token, and otherwise changes state only through explicit
account actions. See [SECURITY.md](SECURITY.md) for the full trust statement.

![cctop](https://raw.githubusercontent.com/ryanirl/cctop/main/docs/hero.png)

Expand Down Expand Up @@ -66,14 +66,15 @@ cctop --no-limits # skip the usage fetch (no network, session table only)

cctop setup # pick a provider; hand off to its agent to help configure
cctop accounts # list discovered accounts (read-only)
cctop switch [NAME] # hot-switch main Claude sessions (or choose the best)
cctop doctor # read-only self-check (platform, binaries, token/expiry)
cctop config init # write a starter ~/.config/cctop/config.toml (optional)
cctop add-account # provision a new account (dry-run; see Accounts below)
```

TUI keys: `r` refresh now · `R` refresh token · `a` add account · `s` stats ·
`,` settings · `q` quit. The footer shows a live countdown to the next auto
refresh; `r` refreshes immediately and resets it.
TUI keys: `r` refresh now · `R` refresh token · `x` hot-switch account · `a`
add account · `s` stats · `,` settings · `q` quit. The footer shows a live
countdown to the next auto refresh; `r` refreshes immediately and resets it.

### Token refresh (`R`)

Expand All @@ -82,10 +83,9 @@ lazily when you *use* an account, so an account you are merely monitoring drifts
past expiry and the usage read starts failing (`token expired`). Press `R` and
cctop asks the tool that owns the credential to renew it: it runs
`claude mcp list` under each account's config dir (a quota-free command whose
startup renews and rewrites the Keychain record). **cctop never writes a
credential itself** — it only triggers the owner binary and reads the result —
and it is a no-op on tokens that are still valid. An account whose refresh token
is itself dead reports "needs re-login" (only a fresh `/login` can fix that).
startup renews and rewrites the Keychain record). This delegated refresh is a
no-op on tokens that are still valid. An account whose refresh token is itself
dead reports "needs re-login" (only a fresh `/login` can fix that).

### Accounts

Expand All @@ -102,6 +102,64 @@ existing config, and signs you in. It is **strictly additive** — it never
deletes, overwrites, or modifies existing config, credentials, or sessions, and
only writes a shell alias if you explicitly ask for one.

### Hot-switch mode

The original separate-session mode remains the default. To keep one main
`~/.claude` session history and use the other config directories only as saved
login profiles, enable:

```toml
[settings]
hot_switch = true
main_config_dir = "~/.claude"
auto_switch_remaining_percent = 1
```

Press `x` (or run `cctop switch NAME`) to swap the healthiest saved login into
the main Keychain/config store. No aliases or account mapping are required:
cctop identifies logins by their Claude organization identity, collapses
duplicate login directories automatically, and labels them from their saved
email address. If the active login exists only in the mutable main store, cctop
automatically preserves it under `~/.config/cctop/profiles/` before switching.
Before each swap, cctop copies the main credential back to its active saved
profile so refresh-token rotation is retained. Claude Code's own advisory locks
make the update safe around running sessions; they pick up the new login after
the macOS Keychain cache refreshes.

While the TUI is running, cctop automatically chooses the healthy Claude
profile with the most headroom when the active profile reaches 1% remaining
(99% used). This threshold is configurable with
`auto_switch_remaining_percent`; a profile that is itself past the threshold is
chosen only when it still has strictly more headroom than the active profile.
That lets a 93%-used login take over from one at 100% without allowing two
equally exhausted logins to ping-pong. Disable `hot_switch` at any time to return
to the original per-directory session view — the saved profiles cctop manages
for itself stay out of the account list while it is off.

For rotation that does not depend on keeping the TUI in the foreground, run
`cctop autoswitch`. It polls immediately and then at `limits_refresh_seconds`,
refreshes dormant saved logins, and applies the same rotation policy as the TUI.
It is suitable for a user service such as macOS launchd. Use
`cctop autoswitch --once` to verify the configured fleet without starting the
long-running supervisor. Successful usage readings are shared through a local
metadata-only cache, so another cctop process still shows the last percentages,
reset times, and reading age when Anthropic temporarily returns HTTP 429.
Locally expired profiles are refreshed before the usage request; a dead refresh
token is shown as `needs re-login` rather than being masked by a fleet-wide 429.
An incomplete cache never hides accounts that lack a successful reading.

Keeping the saved profile in sync is what avoids re-logins: an access token
lasts ~12-15h, but the refresh token behind it is what actually keeps an
account alive, and it rotates as Claude Code uses it. cctop copies the current
main credential back to its saved profile before normal swaps and limits polls
while that credential is valid, so a profile is never reactivated with a
superseded refresh token. Only a refresh token that is genuinely dead needs
`/login` again, and nothing local can substitute for that OAuth round trip. If
the live main credential is rejected,
cctop first asks Claude Code to refresh it; when that refresh token is dead,
cctop preserves the saved copy and immediately activates a ready profile. This
recovery does not depend on the usage endpoint being available.

### Configuration (optional)

cctop needs no configuration. If you want to rename, hide, reorder, or add
Expand Down
29 changes: 21 additions & 8 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ not do, because a tool that sits next to your credentials should be auditable.
| macOS Keychain item `Claude Code-credentials-<hash>`, or `~/.claude*/.credentials.json` | the OAuth token used to authenticate the two GETs above |
| `~/.codex/auth.json` | the token used to authenticate the Codex usage GET |

All of it is read-only. The two usage GETs are plain reads that consume no
message quota.
The two usage GETs are plain reads that consume no message quota. Credential
writes happen only when hot-switch mode is enabled, as described below.

## What cctop does with your token

Expand All @@ -28,13 +28,14 @@ message quota.
account.
- The token is placed in the `Authorization` header of the usage GET and used
nowhere else.
- The token is **never** logged, printed, written to disk by cctop, included in
`--json` output, or transmitted anywhere except the provider's own API
(`api.anthropic.com` / `chatgpt.com`) that issued it.
- The token is **never** logged, printed, included in `--json` output, or
transmitted anywhere except the provider's own API (`api.anthropic.com` /
`chatgpt.com`) that issued it. In hot-switch mode, complete credential
records move only between their existing local Keychain/file stores.

## The only things cctop ever writes

cctop is read-only except for two explicit, additive actions you invoke:
cctop is read-only except for these account-management actions:

1. **Account provisioning** (`add-account`, the `a` key): creates a new
`~/.claude-N` directory; with `--alias` appends one shell-rc line (after
Expand All @@ -46,9 +47,21 @@ cctop is read-only except for two explicit, additive actions you invoke:
**Claude Code binary itself** renews its own token at startup (a quota-free,
non-interactive command). cctop does not write the credential; it delegates
to the tool that owns it.
3. **Hot switching** (`hot_switch = true`, the `x` key, `cctop switch`, and the
configured automatic threshold): copies a complete saved credential record
into the main Claude Code Keychain store and replaces only the
`oauthAccount` field in the main config. Before switching away, the current
main credential is synced back to its matching saved profile so a rotated
refresh token is preserved. If the active login has no stable saved profile,
cctop creates one automatically under `~/.config/cctop/profiles/<org-id>`;
its credential remains in a per-profile Keychain item (or the existing
file-backed format), while the identity file contains only Claude's account
metadata. Writes are performed under Claude Code's own advisory
credential/config locks; file-backed credentials use atomic replace, and
Keychain payloads are sent through `security -i` stdin rather than exposed
as plaintext process arguments.

There is deliberately **no** delete, logout, or credential-writing path in
cctop's own code.
There is deliberately **no** delete or logout path.

## Reporting

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "cctop-tui"
version = "0.2.0"
version = "0.3.0"
description = "A live terminal monitor for multiple Claude Code and OpenAI Codex accounts, in the spirit of nvtop/htop."
readme = "README.md"
requires-python = ">=3.11"
Expand Down
Loading