Skip to content

About

A simple and reliable auto account switcher for Claude Code: rotates to a fresh account before the 5-hour or weekly rate limit hits, so no pane ever stops at the usage quota. A Herdr plugin.

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

grazr

grazr

Rotational grazing for Claude Code: moves the herd to a fresh account before the pasture runs out, in the middle of a message and with no restart.
A Herdr plugin.


senadaruc's fork. This is wazum/herdr-grazr 0.4.7 with per-model weekly limits (MODEL_LIMITS=1), leaving an account the server refuses, grazr.py swap <account>, and toast forwarding for cmux. The plugin id stays wazum.grazr, so it replaces the original in place: config, enrolled accounts and parked credentials carry over.

CI macOS and Linux Python 3.9 or newer Herdr 0.8.0 or newer no dependencies MIT licence

A Claude pane goes dead when the 5-hour or weekly limit hits. Getting it back means logging in as another account by hand. grazr does not wait for the wall. It reads the usage Claude reports after every message, and when a window is nearly spent it swaps which stored credential the official claude binary reads to a fresh account. Claude picks that up on its next request, so the switch lands mid-message. No restart, no background daemon on a timer, no prompt to answer.

You do nothing. There is no command to run and no prompt to answer. A pane mid-task keeps going on the fresh account, and a pane you were not watching never shows that anything changed. You keep working as if nothing had happened, because from where you sit, nothing did.

This is best effort. Claude's status line is debounced and a slow update can be cancelled, so a pane that burns through its last percent inside one message can still hit the wall. What to do then is under "What it will not do".

Requires two or more Claude subscriptions that are all yours. The badges above carry the rest.

How it works · Install · When the server refuses an account · Swap on demand · After a swap · What it will not do · Policy

How it works

Flowchart of grazr's response to each Claude message: it reads the usage Claude hands its status line, stays put while the account has room, and otherwise swaps the stored credential to a fresh account, which Claude picks up on its next request.

You log in to each account once, with Claude's own claude auth login. grazr never sees a password and never mints a token. It copies the credential that a normal login already produced into a parked copy of its own, then copies it back when that account's turn comes. The claude binary is unmodified, and it re-reads its credential on its next request. That is what makes the swap invisible.

grazr learns how much is left from Claude itself. After every message Claude hands its status-line command the session's five-hour and weekly usage. grazr sits in that command. It passes the payload on to the status line you had, shows that line unchanged, and keeps the reading. When a threshold is crossed, a separate process makes the swap. Claude cancels the status-line command when the next update arrives, and a swap must never be caught half done by that. grazr polls nothing and calls no endpoint of its own.

grazr keeps parked credentials where Claude keeps the live one. On macOS that is the keychain, and no secret ever touches the disk. On Linux there is no keychain: Claude's own login is a file only you can read, and a parked credential is stored the same way, in the state directory. The other state files hold only names, identities and the last known headroom.

Install

herdr plugin install senadaruc/herdr-grazr

Then enrol each account:

herdr plugin pane open --plugin wazum.grazr --entrypoint enrol

Pick s for the account you are logged into now. Pick l for each extra one. l logs in through a throwaway config directory, so your live session is never logged out. One keypress, no Return. q or Esc closes the pane without changing anything.

Enrolling also connects grazr to Claude's status line and keeps the one you have. If you later change statusLine in ~/.claude/settings.json by hand, grazr sees no usage. It says so in a toast when a Claude pane starts, and the action grazr: connect to Claude's status line puts it back. The disconnect action restores your previous status line.

Upgrading from 0.2? Run that action once, and remove LIVE_USAGE_BELOW from config.env:

herdr plugin action invoke wazum.grazr.install

Then list them in the order you want them used:

$EDITOR "$(herdr plugin config-dir wazum.grazr)/config.env"
REMAINING_SESSION=15     # rotate when the 5-hour window has less than this left
REMAINING_WEEKLY=10      # what is left below this expires unused at the weekly reset
ACCOUNTS="work personal" # preference order, first with headroom wins
ENABLED=1
DRY_RUN=0                # 1 = log the decision, do not swap
MODEL_LIMITS=0           # 1 = also watch per-model weekly limits (see below)
PARKED_POLL_MINUTES=0    # re-read parked accounts this often (see below)

grazr never moves back to an account just because it recovered. The one exception is a weekly reset. When the week on the account you are on has just reset, and another account still has at least ten points more than the weekly threshold in a week that ends sooner, grazr moves there. What that account has left goes first, or it expires unused at its reset. Less than ten points is not worth two swaps.

The same happens in the last day of an account's week. A session swap hours before a weekly reset leaves that week's remainder parked, so once the parked account's week ends within 24 hours, its session has room again, and it has those ten points, grazr goes back for it.

When every account is below the thresholds, grazr moves to the one with the most left on its lowest window, once that is ten points more than the account you are on. Otherwise it stays, and you keep working until the wall.

Whatever an account has left when grazr moves off it stays there until that window resets, so a large weekly margin is a large weekly loss. Raise it only if you use the same account elsewhere, such as on claude.ai, and want a reserve for that.

Start with DRY_RUN=1 for a day. Every decision is written to grazr.log in the plugin's state directory, ~/.local/state/herdr/plugins/wazum.grazr, with a timestamp, since the status line's own output is the bar. The log also says what a window had left when it reset. If that is often well above your threshold, the threshold is higher than it needs to be. After a swap it logs the first reading on the new account next to what that account had when it was parked. The drop is what the swap cost: a token refresh and a cold prompt cache on every pane.

Turn Herdr's toasts on

Herdr toasts are off by default, so grazr's notification will not show until you turn them on. herdr notification show also exits 0 when it shows nothing, so grazr reads the JSON to find out. Add this to ~/.config/herdr/config.toml:

[ui.toast]
delivery = "herdr"
delay_seconds = 1

[ui.toast.herdr]
position = "bottom-right"

Then run herdr server reload-config. Pick terminal instead of herdr if you work over SSH, or system for macOS Notification Centre.

A plugin toast lasts about three seconds and there is no setting for it, so grazr keeps the text to one short line and plays a sound. Pick system or terminal delivery to read it at your own pace, or set HERDR_DISABLE_SOUND=1 for silence.

Herdr drops a toast while another one is on screen. For the "every account is low" message, grazr notices and says it again next time instead of assuming you read it. A rotation is announced once, so a dropped toast still leaves the swap in grazr.log.

Per-model weekly limits

Some plans cap a model on its own, with a weekly allowance that runs out while the account still has plenty of its all-models week left. Claude does not put that cap in the status line, so by default grazr never sees it, and a pane on that model hits the wall on an account grazr thinks is fine.

MODEL_LIMITS=1 closes that gap. grazr then also asks Claude's usage endpoint, the one behind /usage, for the live account's per-model limits: once every two minutes at most, from the detached process that makes the swap, never from the status line itself. A per-model limit counts only while a pane is on that model, so running out of one model does not move a pane working on another. The status screen shows it as, say, Fable weekly 0% left.

It is off by default for two reasons. The endpoint is undocumented, so a Claude release can change it without notice, and then grazr simply stops finding per-model readings. And grazr learns an account's per-model headroom only while that account is live. It never refreshes a parked token to ask, so a swap can land on an account that is also out of that model, and grazr moves on after the next reading.

Parked accounts

grazr reads an account's usage only from the status line of the account in use, so a parked account's reading freezes at the moment grazr left it. One the server has refilled, or whose limit was lifted, still reads as spent until its reset, and grazr will not move into it.

PARKED_POLL_MINUTES=10 fixes that. Every ten minutes, from the detached step that makes the swap and after it, grazr asks Claude's usage endpoint for each parked account with that account's own saved login, and takes the answer as it comes, headroom put back included. A parked access token that has lapsed is refreshed first, the way Claude Code refreshes it, and the new pair is stored straight back. The refresh token rotates, so the refresh runs under the rotation lock: no swap can read the parked login half way. The live account is never refreshed here; Claude owns that login. grazr.py refresh reads every account at once, the live one included, whatever the interval.

It is off by default: the usage endpoint and the token request are Claude Code's own, undocumented, and a release can change them. A refresh that fails leaves the parked login as it was; one the server accepted but grazr could not store is logged, and that account has to be enrolled again.

When the server refuses an account

Usage is not the only way an account stops. A lapsed subscription, a billing problem, an organisation that turned off Claude Code, a revoked login or a rate limit the status line never got to warn about all end the turn with an error instead. The status line never runs for a refused request, so grazr also connects to Claude's StopFailure hook. The connect action adds it next to any hooks you have, and the disconnect action takes only its own out. Upgrading? Run the connect action once.

On oauth_org_not_allowed, billing_error, account_on_hold or authentication_failed, grazr marks the account as failed and moves on: to the next one in ACCOUNTS with headroom, or, when none has any, to the one with the most left, since an account below your threshold still answers. A failed account is never a target again, not for a threshold, a weekly handover or the swap key, until you enrol it again or a pane on it reports usage that has gone down, which only an answered request can produce. The status pane shows it as FAILED.

On rate_limit the account's lowest open window is set to nothing, and the usual decision takes over, so it comes back by itself when that window resets. With no window on record, it sits out an hour.

Every pane and teammate on the account fails at once. Only the first one acts. The rest find another account live, and a failure in the first minute after an account arrives belongs to requests sent before it did. Claude re-reads its credential before its next request, so a pane that failed only needs its next message: press Esc and send again. A cleared /goal has to be set again.

Swap on demand

Sometimes you do not want to wait for the threshold. The session is at 93% and a long task is about to start, so you would rather be on a fresh account first. grazr has an action for that. Bind it to a key in ~/.config/herdr/config.toml:

[[keys.command]]
key = "prefix+shift+s"
type = "plugin_action"
command = "wazum.grazr.swap"
description = "grazr: swap account now"

Then run herdr server reload-config. The key moves you to the first account in ACCOUNTS with headroom, which is the choice the automatic path would make, and announces it in the usual toast. It does not ask whether the account you are leaving still has room. You asked for the move, so it moves. DRY_RUN=1 applies here too. ENABLED=0 does not, since that flag only quiets the automatic path. If every other account is spent, the key swaps nothing and says so in a toast, naming the account that frees up first and the window you are waiting on, and in herdr plugin log.

To move to one account in particular, name it. Outside Herdr, hand it the directories Herdr would, as the status line does:

HERDR_PLUGIN_STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/herdr/plugins/wazum.grazr" \
HERDR_PLUGIN_CONFIG_DIR="$(herdr plugin config-dir wazum.grazr)" \
HERDR_BIN_PATH="$(command -v herdr)" \
python3 path/to/grazr.py swap personal

The swap is the same one the key makes, with the same lock, toast, log and DRY_RUN, only the target is yours. Any enrolled account can be named, by name or id, listed in ACCOUNTS or not, and its headroom is not consulted either. Naming the account you are on, one nobody enrolled, or one the server refused until you enrol it again, swaps nothing and says so. If the account you name is below a threshold, the next message moves you on again, unless ENABLED=0. Herdr actions take no arguments, so this one is for scripts and for clients that let you pick the account.

After a swap

grazr's toast after a rotation: now on personal

grazr says which account it moved to, in a toast and in grazr.log. To see what is enrolled and what each account has left:

herdr plugin pane open --plugin wazum.grazr --entrypoint status

Remote Control disconnects

Claude's Remote Control belongs to the signed-in account, so it disconnects on every swap. Restart it with /remote-control in each pane you care about. grazr says so in its notification, but it will not type the command for you. That would mean writing into panes that may be mid-turn, and grazr never touches the screen.

The account in your sidebar

A toast is gone in three seconds. grazr also publishes the active account as a $grazr token on every Claude pane, so the sidebar keeps saying which account you are on.

grazr's tag in the Herdr sidebar: this pane is on the account named work

Herdr shows a token only where your own sidebar row asks for it. Add $grazr to a row in ~/.config/herdr/config.toml, keeping whatever rows you already have:

[ui.sidebar.agents]
rows = [
  ["state_icon", "workspace", "tab"],
  [{ token = "agent" }],
  [{ token = "$grazr", fg = "#b5ead7" }],
  [{ token = "terminal_title_stripped" }],
]

Each entry in rows is one line, and a row with three tokens on it runs out of width, so give the tag its own.

Then run herdr server reload-config. A pane picks up the tag when Claude starts in it, and a rotation repaints every pane. Panes already open are still blank, so fill them in once:

herdr plugin action invoke wazum.grazr.tag

A pane you have scrolled up in keeps its old tag until you scroll back down, because repainting one can jump it to the bottom.

What it will not do

  • Act on a reading from an account it has left. A session keeps reporting the old account until its next request, and grazr tells those readings apart by the window's reset time and drops them.
  • Touch a pane that already hit the wall. grazr swaps before that, and after a refused request it swaps away, but it never types into the pane. Press Esc and send again, and it goes out on the new account.
  • Write a credential it cannot write whole. macOS security quietly truncates an over-long input and destroys the item, so grazr measures first and refuses.

Policy

grazr rotates between subscriptions that all belong to the person running it. It never shares credentials, never routes requests through another client, and never changes the claude binary.

Anthropic's public documents do not limit how many accounts one person may have, and they do not cover switching between two of your own plans. The Claude Code docs call CLAUDE_CONFIG_DIR "Useful for running multiple accounts side by side", and they list switching accounts as a normal answer to a usage limit.

Two clauses point the other way, and no document settles them. Advertised limits "assume ordinary, individual usage of Claude Code". The Consumer Terms forbid reaching the services "through automated or non-human means, whether through a bot, script, or otherwise". grazr is a script on the credential path, not on the request path. But Anthropic has not said whether chaining personal plans counts as ordinary use, and it can enforce without notice. Anthropic's own answer to a limit is usage credits. Read the current terms and decide for yourself.

About

A simple and reliable auto account switcher for Claude Code: rotates to a fresh account before the 5-hour or weekly rate limit hits, so no pane ever stops at the usage quota. A Herdr plugin.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages