CCCandybar
Powerline statusline for Claude Code — daemon-backed, config-driven, zero-config-file required.

Try the live bar in your browser → The real daemon, compiled for the page, replays a few turns of a Claude Code session; every click in the bar is real.
What it is
CCCandybar is a statusline renderer for Claude Code. It shows session cost, context usage, git status, model info, rate-limit utilization, and more, configured through a JSON5 DSL with hot-reloading.
A background daemon caches git state, usage data, and per-session values across concurrent Claude Code sessions. The renderer connects to the daemon over a Unix socket, so every invocation is fast (~50ms budget) and stateful.
Quick start
pnpm dlx @promptctl/cc-candybar@latest install
npm is the only install channel: the GitHub releases are tags plus a changelog with no downloadable assets, and installing from a GitHub tag does not work (the built dist/ is not committed).
pnpm 11 delays freshly published packages (minimumReleaseAge), so on or near a release day @latest can silently resolve to an older version. install compares what it staged against the registry and warns when it is behind; to get a specific release immediately, name it: pnpm dlx @promptctl/cc-candybar@<version> install.
That single command (re-run it any time to update to the latest release):
- Stages the runtime at a stable path independent of any package-manager cache —
~/Library/Application Support/CCCandybar/on macOS,$XDG_DATA_HOME/cc-candybar/on Linux — asbin/cc-candybar(the prebuilt native render binary for your platform, or a node entry where none exists) besidedist/index.mjs(the daemon + CLI bundle). - On macOS, builds
~/Applications/CCCandybarURLHandler.appand registers thecc-candybar://URL scheme with Launch Services. - Writes the staged
bin/cc-candybarpath as the statusline command into~/.claude/settings.json(skipped with a notice if you've customized the command;--forceoverwrites).
Restart Claude Code. The statusline appears with the bundled default layout — an identity row (host, directory, gitaculous) over a status row (model, context, autocompact, cacheTimer, block, weekly, activity). On macOS, Cmd-clicking clickable cells fires cc-candybar:// URL verbs that the daemon dispatches (via the URL handler registered in step 2).
Customization
Drop a .cc-candybar.json5 (or .cc-candybar.json — both extensions are accepted) at any of these locations:
$CC_CANDYBAR_CONFIGin the statusline command's environment (~is expanded, and a relative path is relative to the directory Claude Code runs the statusline in). It is read by the client on every render and carried to the daemon, so it can differ per session. A path that names no file renders the bundled default under aConfig file not foundwarning, and one the daemon cannot stat (behind an unsearchable directory, say) underConfig file could not be readwith the error — either way the bar stays live and loads the file once it can.<project>/.cc-candybar.json5(then.jsonat the same location)<cwd>/.cc-candybar.json5(then.json)$XDG_CONFIG_HOME/cc-candybar/config.json5(then.json; defaults to~/.config/cc-candybar/config.json5)
Without $CC_CANDYBAR_CONFIG, every file found is a layer: your user file merges over the bundled default, the cwd's over that, and the project's on top. So a project file declares only what that project adds or changes — a segment placed in a row, a plugin listed — and keeps the rest of your own config. $CC_CANDYBAR_CONFIG names the one file the session loads, and the others are not read. A change made from the bar (a theme picked and saved, a segment added in edit mode) is written to your user file, never to a project's; when a project file sets the same value, the click is refused and names that file.
A project or cwd file, and a repository's own plugins (.cc-candybar/plugins/), come from somebody's repository, and a config can run commands. So the first time a session works in a directory holding one, the bar asks — a cc-candybar config for <dir> — run it? [approve] [decline] — and until you answer, it draws your own config and runs nothing from that directory. The answer is kept per directory of the repository and covers that directory in every linked worktree git worktree add made from its checkout, so a new worktree never asks again. (A checkout whose .git is kept elsewhere, such as a submodule, answers apart from its worktrees.) It is kept in $XDG_STATE_HOME/cc-candybar/repo-configs/repo-configs.json (default ~/.local/state/cc-candybar/) and holds across later edits of the files, as Claude Code's answer for a project's MCP servers does. Remove a directory's entry there to be asked again. cc-candybar check names a directory still awaiting an answer and runs nothing from it.
JSON is a strict subset of JSON5, so the same parser handles both — .json5 is the documented format (supports inline comments, trailing commas, unquoted keys), .json is the legacy/compat extension. When both exist at the same location, .json5 wins and the bar shows a persistent warning so you can remove the shadowed duplicate.
The file merges onto the bundled default, so it declares only what differs. globals merge per field; the named sections (variables, segments, actions, helpers, …) merge by name, your entry winning; root merges per named row (identity and status in the bundled default; a whole tree at root replaces every row). The bundled default is src/config/default-dsl-config.ts, and fuller starting points live under examples/. Run cc-candybar check <path> to load a file exactly as the daemon will: exit 0 means it renders, exit 1 prints the error with the path and line to fix. Add --render and it prints the bar it rendered, so you can read a config's result without installing it.
// A theme, one new segment, and the row it joins. Everything else — the
// identity row, every other segment and variable — is inherited.
{
globals: { palette: 'catppuccin-mocha' },
variables: {
clock: { kind: 'time', layout: '15:04' }, // Go reference-time layout, re-read every second
},
segments: {
clock: { template: '{{ .clock }}' },
},
root: { rows: { status: { h: ['model', 'context', 'clock'] } } },
}
Saving the file triggers a hot-reload of every active session.
Architecture
┌─────────────┐ Unix socket ┌──────────────────┐
│ Claude Code │ ──────────────► │ cc-candybar daemon│
│ (hook) │ render req │ │
│ │ ◄────────────── │ git cache │
└─────────────┘ ANSI output │ usage cache │
│ session state │
│ render cache │
└──────────────────┘
- Daemon (
src/daemon/) — long-lived background process. One per user. Caches git state via filesystem watchers, usage data, and per-session key/value state. Runs until it exits on a memory backstop — on what the platform charges the process:phys_footprinton macOS, RSS on Linux (default 1024 MB viaCC_CANDYBAR_RSS_LIMIT_MB; the V8 heap cap the spawner passes is twice that, a margin wide enough that the graceful backstop fires first under any growth its 60 s poll can see) or the host restarts; there is no idle or age timeout. - Client (
rust-client/src/main.rs) — the Rust binary installed asbin/cc-candybar, and the only thing on the per-render path: it connects to the daemon, sends the hook JSON, and prints the ANSI response. On a daemon miss it spawns one detached and prints an empty line, so the next refresh finds a warm daemon. Every subcommand (install,daemon,check,doctor, …) execs the Node bundle atdist/index.mjs, which carries a socket relay of its own (src/daemon/client.ts) as the fallback when no native binary is staged. - Renderer (
src/dsl/render.ts) —renderDslis the single render path; the daemon, the demo and the tests all call it. It walks the config's layout tree, evaluates each segment's templates against the daemon's payload, and hands the styled cells tosrc/render/(strip and joiner selection, pickers and menus, the diagnostics viewer). The payload's data comes from the providers insrc/segments/— git, session, context, metrics, tmux, pricing. - Themes (
src/themes/) — palette construction plus name policy (which theme, style, variation, endcaps and colour depth names resolve to what). The colour arithmetic itself — blending, contrast, OKLCH transposition — lives in the separaterich-jspackage; this repo does none of it.
Segments
These are the segment names DEFAULT_DSL_CONFIG declares, each available to a user's root whether or not the default bar places it.
| Segment | Shows | Visible |
|---|---|---|
directory |
the cwd, abbreviated fish-style (~/c/cc-candybar) |
always |
model |
✱ and the model's display name |
always |
sessionId |
⌗ and the first 8 characters of the session id |
always |
version |
◈ v and the Claude Code version |
always |
tmux |
tmux: and the tmux session name |
inside tmux |
host |
⇄ user@host |
over SSH |
gitaculous |
the git state, collapsed to ⎇ branch, ahead/behind and S/U/?/! flags; the ▸ at its right edge expands it to (git), repo, operation, sha, flags, branch, upstream ±, stashes and time since the last commit, and ◂ collapses it again (per session). Each form and piece is overridable (docs/segment-authoring.md) |
in a repo |
gitPr |
⇆ #N linked to the pull request, or ⚠ PR when the forge lookup failed |
when a PR or a lookup error is known |
toolbar |
click affordances — copy the session id, open the project dir, the transcript and the repo page (also inside the settings menu) | always |
commands |
type /compact, /model or /clear into this session; /clear asks for a second click (also inside the settings menu) |
inside tmux (not placed by default) |
themeSwitcher |
◀ <theme> ▶ — the theme the bar is wearing; ◀/▶ switch this session to the previous/next theme |
always (not placed by default) |
session |
§ this session's cost and tokens, plus budget status |
always |
today |
☉ today's cost and tokens across sessions, plus budget status |
always |
block |
◱ the 5-hour rate-limit window's utilization |
while that window is active |
weekly |
◑ the 7-day rolling rate limit's utilization |
while that window is active |
burnrate |
⚡ cost per hour, then ETA to the 5-hour and weekly limits |
while either window is active |
speed |
⇅ output, input and total tokens per second |
once the session has tokens |
tokenSparkline |
⚡ a sparkline over the last 24 speed samples |
once speed samples exist |
cacheTimer |
◴ minutes until the prompt cache expires, or cold |
while a cache expiry is known |
context |
◔ context tokens used and the percentage left |
when context tokens are known |
autocompact |
⇲ Claude Code's auto-compact window — auto, or the tokens /autocompact last set; −/+ move it by 100K up to the model's context window, ↺ returns it to auto. Each click types /autocompact into the session, so the controls show only inside tmux. Placed beside context |
always |
metrics |
Δ last response time, ⧖ response time, ⧗ session duration, ◆ message count, lines changed |
when any of those exist |
activity |
⌘ the slash command that opened the turn, ☐ the in-progress todo with its position, ⟳ tools in flight and ✓ tools finished this turn |
while the session is doing something |
Every bar also carries, right after the settings menu's door, a cell saying how it is redrawn: ≤30 fps while an animation lane draws it where you can see it, ≤1 fps while that lane's bar is off screen, and polled when Claude Code reruns the statusline itself. The number is the most frames a second the lane draws; a lane redraws the bar only when it changes, so a still bar is redrawn far less often. It sits wherever the door does, so no layout has to place it, and none can leave it out.
The lit ticket a checkout is working on is not bundled: it is a plugin, installed with cc-candybar plugin install promptctl/links-issue-tracker, whose one segment (plugins.lit.ticket, each of its parts a setting) is in edit mode's add-segment library. It reads the .lit/statusline.json lit keeps in each checkout, so no lit process runs for the bar.
Each segment is a DSL declaration with a template (text + interpolation + style functions), a bg/fg palette spec, and optional when predicate. Templates compose freely — every formatter in the bundled function library (formatCost, formatTokens, formatLongTimeRemaining, budgetStatus, link, urlEncode, the sprig string/list/dict library, …) is available in every segment.
Themes
The DSL config picks a base palette via globals.palette (e.g. textual-dark, gruvbox) — or a template evaluated every render, so the theme can follow live data. Each segment may override with its own palette: field, and bg/fg take a palette colour name (primary, surface, panel, accent, foreground, warning, error, …) or a computed colour such as {{ darken (color "primary") 2 }}; a segment that authors no bg wears the theme's decorative tint for its position. Color math runs through OKLCH for perceptual uniformity.
Installation
Requires Node.js 20.19+ (within the 20.x line) or 22.12+, Claude Code, and Git 2.0+. For best display, install a Nerd Font so the powerline glyphs render correctly.
Manual setup
Run the install once to stage the runtime, then point ~/.claude/settings.json at the staged entry (this is also exactly what install writes):
{
"statusLine": {
"type": "command",
"command": "'/Users/you/Library/Application Support/CCCandybar/bin/cc-candybar'"
}
}
Developing against a checkout
just deploy builds dist/index.mjs and stages the native binary at bin/cc-candybar-native — point your statusline command at that path. On a machine without cargo, pnpm install && pnpm build builds only the bundle — point your statusline at the committed bin/cc-candybar node entry instead. Either way the bar renders HEAD; the daemon watches the built bundle and respawns itself on rebuild.
pnpm bar:web serves the bar at http://127.0.0.1:7317/ in a browser terminal you can click: an isolated daemon (--config my.json5 starts it from a copy of your config, --ssh shows the host segment). pnpm site:build builds the GitHub Pages version into site/dist, the same page with the daemon running inside it against a simulated machine (site/).
Config file
Customization lives in .cc-candybar.json5. See the Customization section above for the resolution order. Saved edits hot-reload — no restart needed.
Contributing
Contributions welcome. See CONTRIBUTORS.md for people who have contributed outside of GitHub PRs.