Skip to content

Commit bc392d7

Browse files
Bogdan Matasaruclaude
andcommitted
feat(statusline): account-aware ccstatusline profiles (enterprise [Timeout] fix)
Enterprise/Team seats return null five_hour/seven_day rate-limit buckets, so ccstatusline's usage widgets render [Timeout] (a back-off label, not a real network timeout). Add a launcher that auto-selects a profile by subscriptionType. - profile-switch.sh: reads subscriptionType only (never the token), caches 5 min, and on enterprise injects a synthetic rate_limits.five_hour.resets_at (from ccstatusline's block-cache) so the 5h timer + monthly credit both render without poisoning ccstatusline's shared per-render usage fetch. - ccstatusline-settings.enterprise.json: 5h reset timer + extra-usage-remaining. - setup.sh installs both profiles + the launcher, wires statusLine to it, and upgrades older installs that still point at plain "ccstatusline"; --check reports the detected account and validates the profiles. - Tests: +section 4b (11 assertions) with a security mock (43/43 pass). - Docs: monitoring-cost-ratelimits, status-line README, CHANGELOG. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 98192a3 commit bc392d7

7 files changed

Lines changed: 350 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@ Claude Code releases.
2020
- One-command `setup.sh` bootstrap (Ghostty + Claude Code) with a 32-assertion test
2121
suite, plus cost / rate-limit monitoring guidance.
2222
- Trust scaffolding: LICENSE, CONTRIBUTING, Code of Conduct, issue/PR templates, CI.
23+
- **Account-aware status line.** A launcher (`assets/statusline/profile-switch.sh`)
24+
auto-selects a `ccstatusline` profile by the account's `subscriptionType`: an
25+
**enterprise/team** profile (5h reset timer + monthly credit) and a **consumer**
26+
profile (5h/7d usage bars). `setup.sh` installs both profiles + the launcher, wires
27+
`statusLine` to it, and upgrades older installs that still point at plain
28+
`ccstatusline`. `--check` reports the detected account and validates the profiles.
29+
30+
### Fixed
31+
- **Enterprise/Team status line showing `[Timeout]`.** Those seats return `null`
32+
`five_hour` / `seven_day` rate-limit buckets, so `ccstatusline`'s usage widgets
33+
rendered `[Timeout]`. The enterprise profile uses widgets that have real data, and
34+
the launcher injects a synthetic `rate_limits.five_hour.resets_at` (from
35+
`ccstatusline`'s block-cache) so the 5h timer and monthly credit both render stably
36+
instead of poisoning the shared usage fetch.
2337

2438
### Changed
2539
- Translated the entire guide and the `setup.sh` bootstrap to professional English.

assets/statusline/README.md

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,57 @@ you've burned and when it resets, without running `/usage`.
1919

2020
[`ccstatusline`](https://github.com/sirmalloc/ccstatusline) is a configurable,
2121
multi-line status line for Claude Code (the 3-line display above). `setup.sh` installs
22-
it and writes this config for you. To do it by hand:
22+
it, ships **two profiles**, and wires a tiny launcher that auto-selects the right one
23+
by your account type (see [Account-aware profiles](#account-aware-profiles-enterprise--team)).
24+
To do it by hand:
2325

2426
```bash
2527
npm install -g ccstatusline@2
2628
mkdir -p ~/.config/ccstatusline
27-
cp ccstatusline-settings.json ~/.config/ccstatusline/settings.json
29+
cp ccstatusline-settings.json ~/.config/ccstatusline/settings.json
30+
cp ccstatusline-settings.json ~/.config/ccstatusline/settings.consumer.json
31+
cp ccstatusline-settings.enterprise.json ~/.config/ccstatusline/settings.enterprise.json
32+
cp profile-switch.sh ~/.config/ccstatusline/profile-switch.sh
33+
chmod +x ~/.config/ccstatusline/profile-switch.sh
2834
```
2935

30-
Then point Claude Code at it in `~/.claude/settings.json`:
36+
Then point Claude Code at the launcher in `~/.claude/settings.json`:
3137

3238
```json
33-
{ "statusLine": { "type": "command", "command": "ccstatusline", "padding": 0 } }
39+
{ "statusLine": { "type": "command", "command": "sh $HOME/.config/ccstatusline/profile-switch.sh", "padding": 0 } }
3440
```
3541

42+
(Prefer the plain line with no account switching? Point `command` at `ccstatusline`
43+
instead — it uses `settings.json` directly.)
44+
3645
Tweak the widgets interactively by running `ccstatusline` in a terminal.
3746

47+
### Account-aware profiles (enterprise / team)
48+
49+
`ccstatusline`'s **5h / weekly usage** widgets read the `five_hour` / `seven_day`
50+
rate-limit buckets from Anthropic's usage API. **Enterprise/Team** seats return those
51+
buckets as `null`, so those widgets show **`[Timeout]`** (a back-off label — not a real
52+
network timeout; the API answers `200` in <0.5s). Enterprise plans expose a monthly
53+
pay-as-you-go bucket (`extra_usage`) instead.
54+
55+
So `profile-switch.sh` reads only your account's `subscriptionType` (never the token)
56+
and picks:
57+
58+
| Account | Profile | Line 2 shows |
59+
| --- | --- | --- |
60+
| **enterprise / team** | `settings.enterprise.json` | `🟢 5h ⟳ <reset> 💳 credit <$ left>` |
61+
| Pro / Max / other | `settings.consumer.json` | `🟢 5h <usage%> ⟳ <reset> 📅 7d <usage%> ⟳ <reset>` |
62+
63+
**The enterprise shim.** `ccstatusline` fetches usage once per render and shares it
64+
across all usage widgets, UNIONing their required fields. `reset-timer` needs
65+
`sessionResetAt`, and Claude Code's payload carries no `rate_limits`, so on enterprise
66+
that field is unsatisfiable and the shared object errors out — which would make
67+
`extra-usage-remaining` show `[Timeout]` too. The launcher injects a synthetic
68+
`rate_limits.five_hour.resets_at` (the 5h block reset, read from `ccstatusline`'s own
69+
block-cache) so the field is satisfied locally and both the timer **and** the credit
70+
render. Detection is cached for 5 min; `rm ~/.config/ccstatusline/.active-profile` forces
71+
a re-check after switching accounts.
72+
3873
### Option B — self-contained `statusline.sh` (no Node, just bash + jq + git)
3974

4075
A single script with no dependencies beyond `jq` and `git`. Lighter, fully yours, but
@@ -50,8 +85,13 @@ cp statusline.sh ~/.claude/statusline.sh && chmod +x ~/.claude/statusline.sh
5085

5186
## Notes
5287

53-
- The **5-hour / weekly** segments rely on your Claude Code version exposing
54-
`rate_limits` in the status payload. If yours doesn't, those segments are omitted
55-
(the model + context + branch still show).
88+
- The **consumer** profile's 5h / weekly **usage %** comes from the usage API; on
89+
enterprise/team those buckets are null (handled by the enterprise profile above).
90+
The **5h reset timer** is computed locally and always works.
91+
- The **credit** widget is API-backed, so it may briefly show `[Rate limited]` /
92+
`[API Error]` after rapid repeated renders — cosmetic, it self-heals on the next good
93+
fetch (cached 180s) and never affects actual Claude usage.
94+
- Check what's active any time: `./setup.sh --check` reports the detected account and
95+
validates both profiles.
5696
- See [Monitor cost & rate limits](../../docs/environment/monitoring-cost-ratelimits.md)
5797
for the full picture, including `/usage` and `/context`.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"version": 3,
3+
"lines": [
4+
[
5+
{ "id": "l1a", "type": "custom-text", "customText": "🤖 ", "color": "cyan" },
6+
{ "id": "l1b", "type": "model", "rawValue": true, "color": "cyan", "bold": true },
7+
{ "id": "l1c", "type": "custom-text", "customText": " 🧠 ", "color": "white" },
8+
{ "id": "l1d", "type": "context-bar", "rawValue": true, "color": "white" }
9+
],
10+
[
11+
{ "id": "l2a", "type": "custom-text", "customText": "🟢 5h ⟳ ", "color": "green" },
12+
{ "id": "l2d", "type": "reset-timer", "rawValue": true, "color": "green" },
13+
{ "id": "l2e", "type": "custom-text", "customText": " 💳 credit ", "color": "blue" },
14+
{ "id": "l2i", "type": "extra-usage-remaining", "rawValue": true, "color": "blue" }
15+
],
16+
[
17+
{ "id": "l3a", "type": "custom-text", "customText": "🌿 ", "color": "magenta" },
18+
{ "id": "l3b", "type": "git-branch", "rawValue": true, "color": "magenta" },
19+
{ "id": "l3c", "type": "custom-text", "customText": " ", "color": "yellow" },
20+
{ "id": "l3d", "type": "git-changes", "color": "yellow" },
21+
{ "id": "l3e", "type": "custom-text", "customText": "", "color": "brightBlack" },
22+
{ "id": "l3f", "type": "session-clock", "rawValue": true, "color": "brightBlack" },
23+
{ "id": "l3g", "type": "custom-text", "customText": " 💾 ", "color": "brightBlack" },
24+
{ "id": "l3h", "type": "free-memory", "rawValue": true, "color": "brightBlack" }
25+
]
26+
],
27+
"flexMode": "full-minus-40",
28+
"compactThreshold": 60,
29+
"colorLevel": 2,
30+
"inheritSeparatorColors": false,
31+
"globalBold": false,
32+
"gitCacheTtlSeconds": 5,
33+
"minimalistMode": false,
34+
"powerline": {
35+
"enabled": false,
36+
"separators": [""],
37+
"separatorInvertBackground": [false],
38+
"startCaps": [],
39+
"endCaps": [],
40+
"autoAlign": false,
41+
"continueThemeAcrossLines": false
42+
}
43+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/bin/sh
2+
# profile-switch.sh — account-aware ccstatusline launcher.
3+
#
4+
# Picks the right ccstatusline profile by your Claude account's subscriptionType,
5+
# then runs ccstatusline with it. Wire it into ~/.claude/settings.json:
6+
# "statusLine": { "type": "command", "command": "sh $HOME/.config/ccstatusline/profile-switch.sh", "padding": 0 }
7+
#
8+
# WHY this exists
9+
# ccstatusline's session-usage / weekly-usage / weekly-reset-timer widgets read
10+
# the five_hour / seven_day rate-limit buckets from api.anthropic.com/api/oauth/usage.
11+
# Enterprise/Team seats return those buckets as null, so those widgets render
12+
# "[Timeout]" (a pessimistic-lock label, not a real network timeout — the API
13+
# answers 200 in <0.5s). Enterprise exposes a monthly pay-as-you-go bucket
14+
# (extra_usage) instead. So we ship two profiles and auto-select.
15+
#
16+
# - settings.enterprise.json : 5h block reset timer + extra-usage-remaining
17+
# - settings.consumer.json : the classic 5h/7d usage % + reset widgets
18+
# - settings.json : ccstatusline default, the fallback
19+
#
20+
# ENTERPRISE SHIM (verified)
21+
# ccstatusline fetches usage ONCE per render and shares it across all Usage
22+
# widgets, UNIONing their required fields. reset-timer requires sessionResetAt;
23+
# Claude Code's statusline payload carries NO rate_limits, so on enterprise that
24+
# field is unsatisfiable and the shared object flips to {error}, which would make
25+
# extra-usage-remaining show "[Timeout]" too. We inject a synthetic
26+
# rate_limits.five_hour.resets_at (the 5h block reset, read from ccstatusline's
27+
# own block-cache) so sessionResetAt is satisfied LOCALLY, never enters the API
28+
# fetch, and the timer + monthly credit both render stably.
29+
#
30+
# Detection is cached for $TTL seconds. No credential value is ever printed; only
31+
# the subscriptionType field is read from the Keychain item.
32+
set -u
33+
34+
CONFIG_DIR="$HOME/.config/ccstatusline"
35+
ENTERPRISE="$CONFIG_DIR/settings.enterprise.json"
36+
CONSUMER="$CONFIG_DIR/settings.consumer.json"
37+
CACHE="$CONFIG_DIR/.active-profile"
38+
TTL=300
39+
40+
CCSTATUSLINE="$(command -v ccstatusline 2>/dev/null || echo /opt/homebrew/bin/ccstatusline)"
41+
PYTHON="$(command -v python3 2>/dev/null || echo /usr/bin/python3)"
42+
43+
# Read subscriptionType from the macOS Keychain (only that field; never the token).
44+
# On non-macOS, fall back to ~/.claude/.credentials.json.
45+
read_subscription() {
46+
creds=""
47+
if command -v security >/dev/null 2>&1; then
48+
creds=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null)
49+
fi
50+
[ -z "$creds" ] && [ -f "$HOME/.claude/.credentials.json" ] && creds=$(cat "$HOME/.claude/.credentials.json" 2>/dev/null)
51+
printf '%s' "$creds" | "$PYTHON" -c 'import sys, json
52+
try:
53+
d = json.load(sys.stdin) or {}
54+
print(((d.get("claudeAiOauth") or {}).get("subscriptionType")) or "")
55+
except Exception:
56+
print("")' 2>/dev/null
57+
}
58+
59+
pick_profile() {
60+
case "$(read_subscription)" in
61+
enterprise|team) [ -f "$ENTERPRISE" ] && printf '%s' "$ENTERPRISE" ;;
62+
*) [ -f "$CONSUMER" ] && printf '%s' "$CONSUMER" ;;
63+
esac
64+
}
65+
66+
# Enterprise only: inject rate_limits.five_hour.resets_at (5h block reset, from
67+
# ccstatusline's block-cache) when the payload lacks rate_limits, so reset-timer's
68+
# required field is met locally and never poisons the shared usage fetch.
69+
enterprise_shim() {
70+
"$PYTHON" -c 'import sys, json, glob, os, time, datetime
71+
try:
72+
d = json.load(sys.stdin)
73+
except Exception:
74+
sys.stdout.write("{}"); sys.exit(0)
75+
if not d.get("rate_limits"):
76+
r = None
77+
bc = sorted(glob.glob(os.path.expanduser("~/.cache/ccstatusline/block-cache-*.json")))
78+
if bc:
79+
try:
80+
s = json.load(open(bc[-1])).get("startTime")
81+
t = datetime.datetime.fromisoformat(s.replace("Z", "+00:00"))
82+
r = int(t.timestamp()) + 5 * 3600
83+
except Exception:
84+
r = None
85+
if r is None:
86+
r = int(time.time()) + 5 * 3600
87+
d["rate_limits"] = {"five_hour": {"resets_at": r, "used_percentage": 0}}
88+
json.dump(d, sys.stdout)'
89+
}
90+
91+
cfg=""
92+
if [ -f "$CACHE" ]; then
93+
now=$(date +%s)
94+
mtime=$(stat -f %m "$CACHE" 2>/dev/null || stat -c %Y "$CACHE" 2>/dev/null || echo 0)
95+
if [ $(( now - mtime )) -lt "$TTL" ]; then
96+
cfg=$(cat "$CACHE" 2>/dev/null)
97+
fi
98+
fi
99+
100+
if [ -z "$cfg" ] || [ ! -f "$cfg" ]; then
101+
cfg=$(pick_profile)
102+
[ -n "$cfg" ] && printf '%s' "$cfg" > "$CACHE" 2>/dev/null
103+
fi
104+
105+
if [ "$cfg" = "$ENTERPRISE" ] && [ -f "$cfg" ]; then
106+
enterprise_shim | "$CCSTATUSLINE" --config "$cfg"
107+
elif [ -n "$cfg" ] && [ -f "$cfg" ]; then
108+
exec "$CCSTATUSLINE" --config "$cfg"
109+
else
110+
exec "$CCSTATUSLINE"
111+
fi

docs/environment/monitoring-cost-ratelimits.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ The [one-command setup](./bootstrap-setup.md) installs this for you — via [ccs
4343
> [!TIP]
4444
> A status line that shows remaining context and time-to-reset is the cheapest insurance against an unexpected limit mid-task. It's the whole point of this guide's setup — you never have to wonder how much plan you've got left.
4545
46+
### Enterprise & Team accounts
47+
48+
The 5-hour / weekly **usage %** widgets read the `five_hour` / `seven_day` buckets from Anthropic's usage API. **Enterprise** and **Team** seats return those buckets as `null`, so the usage widgets render **`[Timeout]`** — a back-off label, not a real network timeout (the API answers `200` in under half a second). Those plans bill against a **monthly pay-as-you-go** bucket instead.
49+
50+
The setup handles this automatically: `setup.sh` installs a launcher (`profile-switch.sh`) that detects your account's `subscriptionType` and swaps to an **enterprise profile** showing the 5-hour reset timer plus your **monthly credit remaining** — no `[Timeout]`. Consumer (Pro/Max) accounts keep the usage bars. Run `./setup.sh --check` to see which profile is active. Details: [status-line assets](https://github.com/bogdanmatasaru/claude-code-guide/tree/main/assets/statusline#account-aware-profiles-enterprise--team).
51+
4652
## What to do when you hit a limit
4753

4854
When you're throttled, you have several quick levers:

0 commit comments

Comments
 (0)