ondelva

← Blog

My Claude Code Status Line Had Four Resident Processes. It Needs Zero.

A status line daemon was quietly running two duplicate render workers and heating my laptop. Fixing it meant deleting the daemon, then finding out Claude Code already sends everything a status line needs on stdin.

Yunseon Hong4 min read

My laptop was running hot with nothing obviously heavy open, so I went looking at what was actually resident. ps turned up a claude-statusbar daemon with a render_worker under it, and then a second render_worker under the same daemon, started fifty-six minutes after the first one. Two workers doing the same one-second-interval render, both counted against the same CPU total.

The status line itself, the little bar at the bottom of the terminal showing quota and cost, was claude-code-usage-bar by leeguooooo (MIT, 377 stars as of 23 September 2026, confirmed via the GitHub API). Three days of accumulated CPU time across the HUD, the daemon, and the two workers came to two hours and thirty-six minutes. Not one heavy spike, just a small tax collected once a second, forever.

What was actually running

802   hud start              36:01
1386  daemon --render-interval 1.0  23:19
1391  render_worker          58:36
14070 render_worker          38:14   ← duplicate

None of that is exotic. A status line you installed once keeps running long after you stopped noticing it, and a duplicate worker from some earlier restart just sits there doubling the tax. I went after it with its own uninstall script first, then the LaunchAgent it had registered (com.leeguoo.claude-statusbar-hud.plist) that respawned the daemon before the files were even gone, then the leftover directories.

The rewrite that started as a lighter reinstall

I wanted the status line back, just lighter, and my first thought was to publish that as a fork under my own GitHub account. Before touching code I checked the license: MIT, so forking, modifying, and redistributing were all fine, the only condition being to keep the copyright notice. No blocker there.

But when I actually looked at what “lighter” meant, there was no code to fork. The whole difference was three settings: auto_upgrade: false, inline rendering instead of a daemon, and a ten-second refresh interval instead of one second. Zero lines changed. Carrying a 28 MB Python bundle around to hold three config values, and rebasing it on every upstream release, was a worse trade than it looked.

So I checked what Claude Code actually sends its status line command, since that decides whether a rewrite is realistic at all. It pipes a JSON blob to stdin on every tick, and the cached copy in ~/.claude/statusline-cache/ showed it already: five-hour and seven-day quota usage with reset times, context window usage, session cost, model name, cache state. Everything the bar needed was already arriving for free. No transcript parsing, no API calls, no daemon required to hold any of it warm.

That settled it. ccbar.py went from a Python prototype to a single 163-line stdlib script, spawned once per tick and exiting immediately.

ccbarthe daemon-backed bar
resident processes03+
install size5 KB~28 MB
per tick~22 ms~86 ms
dependenciesnonePython runtime bundle

The projection line I almost left out

The old bar had one feature I’d stopped noticing until it was gone: a →NN% next to each quota segment, a projection of where usage lands if the current rate holds through the rest of the window. I’d dropped it in the first pass because it felt like the kind of extra I was trying to avoid. Once I checked, the math needed for it was already sitting in the same stdin blob, so leaving it out was just a missed ten lines, not a simplification:

def project(pct, resets_at, window, now=None):
    if not resets_at or not pct:
        return None
    now = time.time() if now is None else now
    elapsed = now - (resets_at - window)
    frac = elapsed / window
    if frac < 0.1 or frac > 1:
        return None
    return min(999, round(pct / frac))

Claude Code sends when a window resets but not when it opened, so the start is inferred as resets_at - window. The frac < 0.1 guard exists because two calls in the first few minutes of a window can look like an infinite burn rate, so the projection stays hidden until at least a tenth of the window has passed. It is a straight-line extrapolation of the whole window’s average, so a heavy burst in the last twenty minutes overstates it. It reads better as a direction than a forecast, and the README says so next to the number.

The bar mixes three numbers that reset on entirely different clocks, which is the part most likely to confuse a glance at 2am:

segmentresets when
5h / 7d quotathe window rolls over, on its own schedule
context window %a new conversation starts
$ costa new conversation starts

The cost figure in particular is the current conversation’s running total, not a daily or monthly one. Open a new session and it starts back at zero, which looks like a discount until you remember it is just a different conversation being billed.

Where it landed

With ccbar wired into statusLine and the old daemon’s official uninstall run, the four resident processes from that first ps (the HUD, the daemon, and the two render_workers) went to zero. Every open terminal session needed a restart to pick up the new config, since each one was still holding the old one-second statusLine command in memory.

ccbar.py lives at github.com/egg-/ccbar under MIT. The copy that actually runs is at ~/.local/bin/ccbar, a plain file copy rather than a symlink into the repo, so moving or deleting the repository later does not take the status line down with it. Updating it just means re-running the install line, on my terms, not the repo’s.

Filed under Build log