PluginWorld
Cc

cc-candybar

Claude Code✓ SPEC VERIFIED

Statusline renderer for Claude Code — a JSON5-configurable DSL with daemon-cached data sources, byte-clean palette-aware composition, and OSC8 click verbs.

@promptctl · MIT · updated 3d ago

SECURITY

A

SCORE

72

INSTALLS

▲ 18.9K

PLUG IN

/plugin marketplace add promptctl/cc-candybar

Then run /plugin install <name> for any plugin it lists

README

CCCandybar

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

License:MIT npm version Dependencies

CCCandybar TUI Mode Demo

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):

  1. 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 — as bin/cc-candybar (the prebuilt native render binary for your platform, or a node entry where none exists) beside dist/index.mjs (the daemon + CLI bundle).
  2. On macOS, builds ~/Applications/CCCandybarURLHandler.app and registers the cc-candybar:// URL scheme with Launch Services.
  3. Writes the staged bin/cc-candybar path as the statusline command into ~/.claude/settings.json (skipped with a notice if you've customized the command; --force overwrites).

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:

  1. $CC_CANDYBAR_CONFIG in 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 a Config file not found warning, and one the daemon cannot stat (behind an unsearchable directory, say) under Config file could not be read with the error — either way the bar stays live and loads the file once it can.
  2. <project>/.cc-candybar.json5 (then .json at the same location)
  3. <cwd>/.cc-candybar.json5 (then .json)
  4. $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_footprint on macOS, RSS on Linux (default 1024 MB via CC_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 as bin/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 at dist/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) — renderDsl is 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 to src/render/ (strip and joiner selection, pickers and menus, the diagnostics viewer). The payload's data comes from the providers in src/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 separate rich-js package; 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.

License

MIT

SIMILAR PLUGINS