cc-cockpit

skill
Security Audit
Warn
Health Warn
  • License — License: MIT
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 6 GitHub stars
Code Pass
  • Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Pass
  • Permissions — No dangerous permissions requested

No AI report is available for this listing yet.

SUMMARY

Claude Code usage panel for GNOME: tray indicator with live rate-limit ring, local dashboard and terminal summary. Reads the official limits from the statusline payload — no credentials, no API calls.

README.md

cc-cockpit

license: MIT
PyPI
AUR

A Claude Code usage panel for Linux: a tray indicator with a consumption ring,
a local dashboard and a terminal summary.

Everything is read from what Claude Code already writes under ~/.claude — or
under each account's directory, if you run more than one. It makes no network
calls and sends nothing anywhere.

One exception is worth stating plainly: to tell you when your login expires it
opens .credentials.json and reads exactly two fields — the refresh-token
expiry and the plan name. The tokens sitting beside them are never read, never
stored and never printed. See cockpit/auth.py.

The interface follows your OS language — English, Portuguese and Spanish are
bundled — and can be pinned in the config file or with --lang.

What it shows

5h block how much the current rate-limit window has consumed, time to reset, hourly pace, projection to the end of the block, and how long until the reference ceiling. The window starts at the exact timestamp of its first request — not rounded to the hour — which is what makes the reset match what the CLI reports
7 days / today / month rolling totals, as a percentage of your own historical peak
Open sessions every live CLI instance: name, project, busy/idle, uptime, RAM, pid, and what that session has consumed. In the tray, clicking one opens a terminal in its own directory, resuming that conversation
Recent sessions the last conversations you can go back to, with their title, project and age. Closing a terminal used to take its session with it — the id lives only in the transcript. The tray folds them into one dropdown, a click away from a terminal; the dashboard hands you the claude --resume line; the report prints it
Projects ranked by consumption across the whole history
Blocks, days and hours time series showing when you actually spend
Official limits over time the real 5h and 7d percentages plotted as they were reported, breaking the line where a window resets rather than drawing a fall that never happened
When the spending happens weekday against hour of day over the last four weeks — the pattern neither the daily nor the hourly series can show on its own
Token mix input / output / cache write 5m / cache write 1h / cache read, with the cache hit rate
Models, effort and subagents where the consumption really goes
Login how long until you have to sign in again — the refresh token's expiry, not the access token's, which the CLI renews by itself every few hours

Usage is measured in API-equivalent USD: what those messages would cost on
the pay-as-you-go API. On a Pro/Max plan none of it is billed — the number works
as a weight unit for consumption and shows how much the plan returns.

The three surfaces

The tray menu is the glance. One section per account with its 5h and 7d
windows, then the day, the month, the sessions open, the ones you can resume and
today's projects. Every session line is a button: clicking it opens a terminal in
that directory resuming that conversation. Shown on the panel picks which
account the label speaks for.

A session line never opens a submenu, and that is not a style choice: on GNOME's
AppIndicator extension an item that carries an icon — every session line does,
for the state dot — exports its submenu correctly and then renders it empty. An
item without an icon submenus fine, which is why Recent sessions folds into a
dropdown and the open ones do not. The detail a session used to expand into —
directory, context window, requests, pid, memory — lives in the dashboard, which
has room for it.

It refreshes every twenty seconds without redrawing. The menu is exported over
dbusmenu and the panel draws it, so adding or removing an item tears the popup
down while you are reading it; a refresh that keeps the same shape only rewrites
the labels that actually changed.

The dashboard is the long look: gauges, sixty days of history, the last
twenty-four hours, projects, blocks, the token mix, models and effort. Open it
from the tray, or cc-cockpit serve --open. With more than one account it grows
a tab bar — one per account, plus All accounts.

Three cards fold: the official curve, the heatmap and the recent sessions. Each
viewer's choice is kept in their own browser, and the recent list ships folded —
it is the one you consult, not the one you watch.

Settings opens a real window rather than a submenu: a menu has nowhere to
type a number, and GNOME's appindicator extension flattens submenus to a single
level anyway. Four tabs — General, Accounts, Limits, Plan — each scrolling on
its own so nothing falls off a short screen, with Save always reachable below
them. Saving applies right away, without a restart.

Install

Debian / Ubuntu — the .deb pulls in the GTK dependencies by itself:

# from the latest release
sudo apt install ./cc-cockpit_0.5.2_all.deb
cc-cockpit setup
nohup cc-cockpit tray >/dev/null 2>&1 &   # tray now, without logging out

Arch — from the AUR:

yay -S cc-cockpit    # or paru, or makepkg -si
cc-cockpit setup
nohup cc-cockpit tray >/dev/null 2>&1 &   # tray now, without logging out

Any distribution — pipx, reusing the system GTK bindings:

sudo apt install python3-gi python3-cairo gir1.2-ayatanaappindicator3-0.1  # tray only
pipx install cc-cockpit --system-site-packages
cc-cockpit setup
nohup cc-cockpit tray >/dev/null 2>&1 &   # tray now, without logging out

--system-site-packages is what lets the virtualenv see PyGObject and pycairo.
Without them the tray is unavailable, and the dashboard and report still work.

From a checkout:

git clone https://github.com/wallacemartinss/cc-cockpit
cd cc-cockpit && ./install.sh
nohup cc-cockpit tray >/dev/null 2>&1 &   # tray now, without logging out

cc-cockpit setup registers the autostart entry, captures the statusline
(see below), checks the tray dependencies and runs the first collection.
cc-cockpit setup --remove undoes the autostart entry.

That autostart entry only fires on the next login, so the last line starts the
tray in the session you are already in — the icon shows up right away, with no
need to log out. It is the same command on every distribution, and it is only
needed once: from the next login on, autostart takes care of it. Running it
again is harmless — a second tray refuses to start and says which pid already
holds it.

cc-cockpit                 # tray + dashboard in the background
cc-cockpit report          # terminal summary
cc-cockpit serve --open    # dashboard only (http://127.0.0.1:8765)
cc-cockpit json            # everything as JSON, for scripting
cc-cockpit collect         # ingest new transcripts and exit
cc-cockpit config          # config path and contents
cc-cockpit accounts        # list the Claude Code accounts it reads
cc-cockpit --lang es report

Desktops

The indicator is a StatusNotifierItem, not a GNOME applet, so it shows up on any
panel that hosts one:

GNOME needs the AppIndicator extension (gnome-shell-extension-appindicator); Ubuntu ships it enabled
XFCE xfce4-panel 4.16+ hosts indicators through Status Tray Items — add that item to the panel. On 4.14, install xfce4-statusnotifier-plugin
KDE Plasma nothing to install
LXQt enable the Status Notifier plugin on the panel
LXDE and other XEmbed-only trays the ring icon still shows, through the fallback in libayatana-appindicator, but the panel label (45% · $12.30) is lost — snixembed brings the indicator path back

cc-cockpit setup prints what your desktop needs. GNOME is what this is
developed and tested on; the others follow from the protocol, not from separate
code paths. The dashboard and report depend on none of it.

More than one account

A company Claude Code in ~/.claude and a personal one in ~/.claude-pessoal
are two subscriptions, not two folders. Each has its own 5h and 7d windows,
so cc-cockpit keeps them apart everywhere: separate history, separate rate-limit
snapshot, separate calibration.

Accounts are not picked up on their own — a stray copy of a Claude Code
directory is not a second subscription, and adding it silently would be worse
than asking. What the tool does instead is say what it found: setup and the
Settings Accounts tab both list any directory that is not set up as an
account, next to the button that adds it.

cc-cockpit accounts --detect          # register what it found, and say where it looked
cc-cockpit accounts --primary empresa # whose number the tray label shows
cc-cockpit setup                      # re-registers the statusline in each one

Detection looks at ~/.claude*, ~/.config/claude*, $CLAUDE_CONFIG_DIR, and
that same variable inside any Claude Code running right now — which is how it
finds an account kept somewhere the globs would never reach. Anything else:
cc-cockpit accounts --add work=/path/to/its/dir, or Add directory… in the
Settings Accounts tab.

That last step is the one that matters. The statusline payload carries the
account's rate limits but nothing that identifies the account, so each
settings.json gets cc-cockpit statusline --account <id>. Without it,
whichever CLI renders last overwrites the other's percentage, and the tray
reports the wrong subscription with nothing on screen to reveal the swap.

What changes where

The tray label speaks for the primary account, and the ring takes the colour
of whichever account is worst off — a 95% on the one you are not watching
still turns the icon red. The menu gains one section per account and a Shown on
the panel
submenu to switch between them.

The dashboard gains a tab per account, plus All accounts. That tab adds
up spend, tokens, projects and models, and deliberately shows one ring per
account instead of a combined percentage: two windows with different ceilings
and different resets have no meaningful sum.

Naming them

Detection names an account after its directory, so ~/.claude becomes
default — a poor label for what is usually the company account. There are two
different things to rename, and they carry different risk:

cc-cockpit accounts --label default=Empresa   # the name shown everywhere
cc-cockpit accounts --rename default=empresa  # the id, moving its history along

The name is editable in the Settings Accounts tab too. The id is not,
because it names accounts/<id>/, which holds months Claude Code has already
pruned; changing it has to move a directory, so it lives in --rename, which
also fixes up the statusline registrations.

By hand

Accounts live in the config file:

"accounts": [
  {"id": "empresa", "label": "Empresa", "dir": "~/.claude"},
  {"id": "pessoal", "label": "Pessoal", "dir": "~/.claude-pessoal"}
],
"primary_account": "empresa"

With nothing configured, everything behaves exactly as before, against
CLAUDE_CONFIG_DIR or ~/.claude. An existing history is moved into
~/.local/share/cc-cockpit/accounts/<id>/ the first time the new version runs.

Configuration

Everything the Settings window writes lives in ~/.config/cc-cockpit/config.json, and any key missing from
the file is written back on start, so new options show up there:

{
  "language": "auto",            // auto (follows the OS) | en | pt | es
  "block_hours": 5,
  "limits": { "block_usd": null, "week_usd": null },  // null = automatic
  "tray_metric": "block",        // block | week | today | none
  "tray_show_cost": true,
  "recent_sessions": 5,          // conversations offered for resuming; 0 turns the list off
  "menu_bar_style": "blocks",    // blocks | shade | fine | dots | squares |
                                 // line | braille | color_blocks | color_dots
  "refresh_seconds": 20,
  "plan_monthly_usd": null,      // e.g. 200 -> shows how many times the plan paid for itself
  "plan_name": "",
  "local_currency": null,        // e.g. {"code":"BRL","symbol":"R$","rate":5.4}
  "dashboard_port": 8765,
  "warn_pct": 70,
  "critical_pct": 90,
  "accounts": [],                // see "More than one account" above
  "primary_account": null        // null = the first one
}

The real numbers, from the statusline

Two things cannot be derived from local transcripts:

  1. The limit belongs to the account, not to the CLI. Whatever you consume in
    the Claude app counts against the same window and leaves nothing on disk, so
    a window can start before your first local request.
  2. The weekly limit is a fixed window with its own reset time, not the
    rolling 7 days a local reader would assume.

Claude Code pipes a JSON payload into the statusline command on every render,
and it carries exactly what the plan panel shows:

"rate_limits": {
  "five_hour": {"used_percentage": 23, "resets_at": 1788800000},
  "seven_day": {"used_percentage": 3,  "resets_at": 1788790000}
}

Register the capture once — no undocumented endpoint, no token:

cc-cockpit statusline --install

It writes statusLine into ~/.claude/settings.json, keeping a .bak. If you
already had one, it is chained rather than replaced, so its output still shows
in the CLI. The captured payload also carries the context window percentage
per session
, which the dashboard shows next to each open session.

From then on the official percentage is the source of truth, and it reveals the
real ceiling — local consumption ÷ official percentage — so the currency
figures stay meaningful too.

When there is no statusline data yet

Numbers fall back, in order of trust: official (statusline) → anchored
(what you typed) → local estimate. The middle one exists because a fresh
install has no capture yet:

cc-cockpit sync --block 23% --block-reset 1h55 --week 3% --week-reset 1h15
cc-cockpit sync            # show anchors, samples and implied ceilings
cc-cockpit sync --reset

Both the tray and the dashboard say which source is in use.

How it works

<account>/projects/**/*.jsonl   transcripts (usage per request)
<account>/sessions/*.json       one entry per live CLI       ─┐
statusline payload (stdin)      official rate limits + context ├─> cockpit/
   ~/.local/share/cc-cockpit/accounts/<id>/events.ndjson <────┘
   ~/.local/share/cc-cockpit/accounts/<id>/panel.json    official snapshot

<account> is CLAUDE_CONFIG_DIR or ~/.claude when nothing is configured,
and each configured account otherwise.

  • collector.py reads each transcript from the last offset, so a refresh
    costs ~30 ms even with 190 MB of history.
  • Events land in a dedicated NDJSON file. That matters: Claude Code prunes
    transcripts after ~30 days
    , and from the first collection onward cc-cockpit
    keeps the full history.
  • Deduplication by message.id:requestId, so resuming a session is not counted
    twice.
  • sessions.py validates each pid against /proc and compares the
    starttime, so a recycled pid is never mistaken for a live session. The
    recent list comes from the transcripts instead — only what is still on disk
    can be resumed — and reads the last 64 KB of each, where Claude Code keeps
    rewriting the generated title and the working directory.
  • Prices live in pricing.py: cache writes at 1.25× (5m) and 2× (1h) of input,
    cache reads at 0.1× (0.025× on Fable 5.1). The transcript separates the two
    cache-write TTLs and the calculation uses that split instead of assuming 5m.
  • i18n.py holds one catalogue for all three surfaces, plus locale-aware number
    and currency formatting.
  • panel.py keeps the official snapshot and appends a line to
    panel-history.ndjson whenever the percentage changes. /api/history reads
    it back for the chart, which is what that file was always for.
  • accounts.py owns the roster and hands every stateful module its directory.
    Only money is ever added across accounts — rate limits, ceilings and anchors
    belong to one subscription and are never mixed.
  • terminal.py knows twelve terminal emulators and what each wants, so a
    session can be reopened where it lives.
  • auth.py reads two fields out of .credentials.json and nothing else. The
    access-token expiry sitting next to them is deliberately ignored: the CLI
    refreshes it by itself every few hours, so showing it would announce an expiry
    that never happens.

Honest limitations

  • Without the statusline capture and without limits, the percentage is
    relative to your own history, not to the real plan limit.
  • The statusline only refreshes while a CLI session is rendering. That is
    enough — what is not running cannot be consuming — but right after a long
    gap the percentage may lag until the next render.
  • Consumption from the Claude app shows up in the official percentage, never in
    the local currency figures, which read Claude Code transcripts only.
  • Models released after this version fall back to their family price (opus,
    sonnet, haiku, fable) until they are added to pricing.py.
  • <synthetic> rows are responses the CLI generates locally: they show up in
    the request count and cost nothing.
  • Opening a terminal on a session starts a new one resuming that
    conversation; it cannot raise the window the session is already in. Window
    activation by pid is not available to an ordinary application on Wayland.
  • With several accounts, the All accounts view adds up money but never
    percentages: each subscription has its own window, and one combined ring
    would be a number that does not exist anywhere.

Packaging

packaging/ holds the .deb build script and the Arch PKGBUILD; see
packaging/README.md for the release flow. A v* tag
builds the wheel, the sdist and the .deb, publishes to PyPI and attaches
everything to the GitHub release.

License

MIT — see LICENSE.

Not affiliated with Anthropic.

Reviews (0)

No results found