GUESTBOOK.EXEcontact.exe
CCLINE

</ccline>

CCLINE

Statusline toolkit for Claude Code. Zero runtime deps, 1035 tests, one cat.

1035

tests, 55 files

0

runtime dependencies

300ms

shared render budget

1.978x

token inflation removed

SPECIFICATIONS

ROLESOLO BUILD
YEAR2026
TYPECLI TOOL
STATUSLIVE · MIT · npx ccline-cli install
STACKtypescript · node.js · cli · claude code · zero deps · vitest · ansi · unicode +1 more
LINKS[live ↗][github ↗]
AVAILABILITYnpmjs.com

A statusline runs on every single prompt render, which makes it the one piece of tooling most able to damage a session it has no business touching, and the usual failure is not a crash but silence.

ccline is a zero-dependency TypeScript toolkit for all four Claude Code extension surfaces: the statusline, hooks, slash commands and output styles.

README.TXT: CCLINE (18 KB)[full readme →]

== WHAT IS THIS ==

────────────────────────────────────────

ccline is a zero-dependency TypeScript toolkit for all four Claude Code extension surfaces: the statusline, hooks, slash commands and output styles. The statusline registers eight segments, seven on by default, and runs every enabled one concurrently against one shared 300ms deadline, and the render path is built so it can never exit non-zero and never write to stderr, because a broken statusline must not touch your session. MIT licensed, installed with npx ccline-cli install.

== </the problem> ==

────────────────────────────────────────

A statusline runs on every single prompt render, which makes it the one piece of tooling most able to damage a session it has no business touching, and the usual failure is not a crash but silence. It also has to be right about numbers that are easy to get wrong: the obvious way to count tokens in a Claude Code transcript is to sum the usage on every line, which inflates the total 1.978x because one assistant message is written as several lines carrying the same cumulative figures, and the obvious way to report a total includes cache reads, which on one measured session outnumbered real input and output 429 to 1. Both mistakes produce a number that is stable, grows monotonically and is roughly the right order of magnitude, so nothing about it looks broken. A tool that fails soft has a third problem on top: a segment that is merely too slow renders empty and looks exactly like a healthy one.

== </my approach> ==

────────────────────────────────────────

ccline is a zero-dependency TypeScript toolkit covering all four Claude Code extension surfaces: the statusline, four hooks, four slash commands and three output styles. Every enabled segment races one shared 300ms deadline concurrently, a failing segment drops itself rather than the line, and the process exits 0 with an empty stderr whatever happens. Token totals are deduplicated by message.id against a rolling window of the last 2000 ids held in an incremental checkpoint, cache reads are excluded by default and restorable by config, and the budget is enforced by a test that renders a cold 1000-message transcript and asserts both that it finishes in time and that real values are still in the output. Because the render path is contractually silent, ccline doctor is the loud counterpart, with 9 check families and a self-test that proves all 25 of its checks can fail. Installation is npx ccline-cli install; the package is MIT and ships no runtime dependencies at all.

== </the story> ==

────────────────────────────────────────

ccline is a statusline for Claude Code, plus the hooks, slash commands and output styles that sit alongside it. It reads one JSON object on stdin per prompt render and prints one line: context remaining, tokens, model, branch, cost, duration. I built it on two rules that end up deciding almost everything else. The first is that the render path never fails loudly. ccline statusline never exits non-zero and never writes to stderr, because a broken statusline must not be allowed to touch your session. Fail-soft is per segment, not per line, so a segment that throws, rejects, returns nothing, or is configured under a name that does not exist drops only itself and leaves the rest of the line intact.

The second rule is that slow is the same as broken. A line that misses its deadline renders empty and looks perfectly healthy while telling you nothing. I know that failure mode because I watched it on an earlier project: a segment asked for a 1520ms read behind a 1200ms abort, lost the race every single time for months, and nobody noticed, because fail-soft made a dead feature look identical to a healthy one. So the budget is asserted, not hoped for. Every enabled segment races one shared 300ms deadline concurrently, and a test renders a cold 1000-message transcript and asserts the whole line finishes inside it. That same test asserts the output still contains 1.2M tok and 18m, because otherwise the timing assertion would pass on a line that rendered nothing. Measured headroom is 5.0ms at 1000 messages and 11.8ms at 5000.

Most of the work turned out to be measurement rather than rendering. Claude Code writes one assistant message as several JSONL lines that all carry the same message.id and the same cumulative usage, so they are restatements, not increments. Summing every line instead of deduplicating by id inflates the total 1.978x: on a real transcript that is 1,056,805,602 naive against 534,220,602 true, over 1108 distinct messages. Cache reads are excluded from the headline figure for the same kind of reason. On one real session they were 348,167,965 against 811,270 of actual input and output, 429 to 1, and a number dominated 429 to 1 by cache traffic is not a number about your work. Both mistakes are invisible unless you go looking, because the wrong number is stable, grows monotonically and is roughly the right order of magnitude.

The pet segment is the one piece of decoration in the tool, and the size is the feature. The reference pet is a cat named bubba, squeezed into one terminal cell using the U+2596 quadrant block characters, so the entire canvas is a 2x2 bitmap and the medium holds exactly sixteen distinguishable pictures. He has five states: appear, alert, tailflick, sleep and idle. He has no whiskers and no eyes, because a quadrant only lights when at least half of it is inked, and a feature smaller than an eighth of the sprite cannot survive that trip. The first rasteriser lit a quadrant on any ink at all, and since every pose has ears, all five poses came out as the same solid block. The frame is a pure function of session state, with no timers and no randomness, because the statusline does not tick, it re-renders when Claude Code decides to, and a pet that jitters next to real numbers makes the real numbers look untrustworthy too. He is off by default, with a test asserting he stays off by default, and he adds zero new filesystem reads: he rides the memoised transcript read that tokens and duration already paid for. A pet that takes eight columns is a pet somebody turns off in a week.

Because the render path is contractually silent, it needs a loud counterpart, so ccline doctor names every symptom the renderer swallows: a settings file in a higher scope silently shadowing the one you just wrote, a stale invocation, a missing transcript, a segment rendering nothing and why. doctor --self-test feeds fabricated broken worlds through the real check functions and currently reports 25 of 25 checks proving they can fail, and a coverage guard fails the build if a new check family ships without a scenario that breaks it. The toolkit is 69 TypeScript files and 13,317 lines with no dependencies key in package.json at all, covered by 1035 tests across 55 files, run on five CI legs.

== </architecture> ==

────────────────────────────────────────

Four surfaces, one package, zero runtime dependencies. The statusline registers eight segments (context, tokens, model, branch, cost, duration, update, pet), seven of which are on by default. renderLine runs every enabled segment concurrently against one shared deadline and never rejects. The concurrency is not a flourish: if segments ran in sequence the budget would have to be divided by the segment count, so adding a segment would silently shrink every other segment's allowance. The deadline timer is deliberately not unref'd, and the unref it used to carry is a bug I shipped and then documented in place. An unref'd timer does not keep the event loop alive, so when the only pending work was a quiet segment with no outstanding I/O, node exited before the deadline fired, renderLine never settled, and the whole line vanished instead of one segment: the exact failure the module exists to prevent, wearing the costume of the mechanism meant to prevent it.

Reading the transcript is the expensive part, so it happens once per render and everything else rides that one read. The parser is incremental. A checkpoint in the cache file records the byte offset, the running totals, and a rolling window of the last 2000 message ids so deduplication survives across reads without the cache growing without bound. The checkpoint also fingerprints which file it belongs to using the first 512 bytes, because a size check alone catches only a replacement smaller than the offset, and a session file replaced by a longer one would resume into the middle of a stranger and keep reporting dead totals forever. On the 208MB transcript I measured against, the first cold render cannot finish inside 300ms, so tokens and duration drop once, a shorter line renders on time, the read completes after the line is written, the checkpoint lands, and every later render is complete at about 48ms. Degrading once beat blocking the prompt for a second at the start of every session.

The small details carry most of the correctness. The branch segment reads .git/HEAD directly rather than taking the transcript's gitBranch field, which reports the branch as of the last assistant message and therefore lies after a git switch until the next turn. Colour is gated on NO_COLOR alone and never on process.stdout.isTTY, because Claude Code captures the statusline's stdout so it is never a TTY: an isTTY check would disable colour permanently for every user while looking entirely reasonable in review. Every rendered part passes through one sanitiser at the boundary that strips C0, DEL and C1 control characters, since before it existed a segment could hide the user's scrollback, erase it, rewrite the window title, split the statusline into two lines, or beep on every render. Bidi overrides and zero-width characters are deliberately left alone, because they are text, not commands.

The art is generated, not checked in. A script holds bubba's five poses as 8x8 silhouette grids and emits the frames module, and its --check flag compares the committed file byte for byte against the generator, so a hand edit is a build failure rather than a silent fork. The same rule covers the images: every screenshot in the README is produced by running the real renderer over pinned fixtures, with a fixed clock so duration cannot tick, an in-memory transcript, a null cwd so branch cannot pick up the generator's own repo, and a literal environment so a developer's NO_COLOR cannot produce a colourless screenshot of a colourful tool. The update notifier is the only code in ccline that opens a socket, and it never runs from the render path: the check runs only from nine named commands a person types, fetches about forty bytes of dist-tags rather than the full packument, and writes the result into the existing cache file that the update segment then reads and nothing else.

== </key features> ==

────────────────────────────────────────

One line, eight segments, one deadline

Context remaining rather than context used, tokens, model, branch, cost, duration and an update marker, seven of them on by default. All of them race a single shared 300ms budget concurrently, so eight slow segments cost one budget rather than eight, and any segment that misses drops itself instead of the line.

Token counting that survives inspection

Deduplication by message.id removes a measured 1.978x inflation, and cache reads are excluded from the headline figure after they came in at 348,167,965 against 811,270 of real input and output on one session. Both are configurable back on for anyone who wants the raw totals.

bubba, the one-cell pet

A cat drawn into a single terminal cell with U+2596 quadrant blocks, five states (appear, alert, tailflick, sleep, idle), no whiskers and no eyes because nothing smaller than an eighth of the sprite survives rasterisation. The frame is a pure function of session state, with no timers and no randomness, so it never jitters. Off by default, pinned by a test, and it adds zero new filesystem reads.

Four hooks that know their limits

notify on Stop and Notification, session-log on SessionEnd rather than Stop so a session is one row and not dozens, auto-format that runs only a formatter the project already depends on and never falls back to npx mid-edit, and guard, a path deny-list over the file-editing tools that says plainly it is not a sandbox because Bash is outside its reach.

Slash commands with narrow grants

/review, /ship, /catchup and /explain ship as TypeScript constants and are written into .claude/commands with an ownership marker in the file rather than a manifest. Each grants read-only git subcommands plus Read, Grep and Glob, and a test fails any bare Bash grant, because a shipped prompt that can run anything is a blank cheque on the user's machine.

ccline doctor, the loud counterpart

9 check families covering config, install state, installed version, transcript resolution, per-segment output, node version, colour, the update check and render time, plus scope shadowing that the render path can never report. doctor --self-test runs fabricated broken worlds through the real check functions and reports 25 of 25 checks proving they can fail.

Generated assets, checked by the build

Every README image is real renderer output over pinned fixtures, and the pet frames are emitted from a generator holding 8x8 grids. A check task fails the build if any committed image or frame file has drifted from what the generator produces.

== </key decisions> ==

────────────────────────────────────────

DECISION 01

The render path never fails loudly, and the cost is that it can never tell you anything either. ccline statusline returns 0 whatever happens, writes nothing to stderr, and drops individual segments rather than the line. That is the right trade for something that runs on every prompt render, but it means a typo like brnach in your config is indistinguishable from a segment that simply has nothing to say. So the loud path is a separate command rather than an exception: ccline segments marks unknown names as unknown, and ccline doctor reads all three settings scopes and names which one wins, because Claude Code prefers a local settings file over a project one over the user one, and a statusline written into a shadowed scope succeeds and is then never visible.

DECISION 02

Token counting deduplicates by message.id and excludes cache reads, which makes the default number smaller than the true total. Deduplication is not optional: the naive sum was 1,056,805,602 against a true 534,220,602, a 1.978x inflation, and the state has to survive incremental reads, so the checkpoint carries a bounded window of the last 2000 ids rather than every id forever. Excluding cache reads is the arguable half, and the argument is that 348,167,965 cache reads against 811,270 of real input and output tracks how often a long conversation was replayed, not how much work happened. The cost is a headline number that is not the whole number, so includeCache turns the full figure back on, and the session-log hook records all four counters as separate fields and never blends them.

DECISION 03

The pet gets exactly one terminal cell, two at the absolute ceiling, and every awkward thing about it follows from that budget. Sixteen distinguishable pictures is the entire medium, which is why there are five states and why five is about the ceiling, and half-coverage rasterisation is why there are no whiskers and no eyes. When the terminal cannot render block characters the pet degrades to nothing at all rather than to an ASCII face, because at one cell wide the alternative is a period or a hash, and a decorative period in the middle of someone's statusline reads as a rendering bug rather than as a cat. The cost is that users on a non-UTF-8 locale get no pet, silently, and the rest of the line is byte-identical. The payoff is that nobody ever has to reclaim eight columns from a joke.

DECISION 04

A module path named in a project config file is never honoured, and third-party authors pay for that with a wrapper script. A .ccline.json arrives with a git clone, so loading a plugin named in it would mean cloning a repository executes its code on your next prompt render. The registry is programmatic only. The same threat model shapes the rest of the surface: the guard hook blocks by printing Claude Code's decision object and exiting 0, never exit 2, so that blocked and crashed are never the same observable event and a non-zero exit from any ccline hook always means the hook itself broke. Every shipped slash command grants narrow read-only git and nothing else, with a test that fails any bare Bash grant, and the cost is real, since /ship has to ask for the test-suite run through the normal permission flow where the user sees it and agrees. Config merging for the update check is one way only: a layer can turn it off, never back on.

DECISION 05

Screenshots are not mockups, and the price is a build step that has to run before the images can be made. Every image in the README comes from the real renderer over pinned fixtures, and a check task fails if a committed image has drifted, on every push in CI. The generator reads the built output rather than source and refuses to run when the build is missing, rather than failing with a module-not-found error, because a stale build would be worse: it would silently screenshot the previous version of the tool. A picture that can drift away from the tool is a lie with a long shelf life, and README screenshots are exactly where that happens.

== </what i learned> ==

────────────────────────────────────────
>

Any timing assertion that does not also assert the work happened is a test that cannot fail. The render-budget test asserts the line lands inside 300ms and that real values are still in it, because a line that rendered nothing would pass the timing half perfectly.

>

A failure path that is contractually silent needs a loud second path, or nobody ever learns anything. Everything the renderer swallows, doctor has to name, and a self-test that proves every check can fail is the only reason to believe the checks work at all.

>

A default can look protective and be provably inert. The guard hook shipped .ssh/** as an anchored pattern, but a home directory is never inside the session directory, so the pattern could not match an absolute path and the files it uniquely covered, config, known_hosts and authorized_keys, were never protected for as long as it shipped.

>

Constraints are what make a small feature good. The pet is worth having because it costs one cell and zero extra reads, and every appealing idea for it, animation, whiskers, a bigger sprite, is an idea that would have made somebody turn it off.

next:

The pet roster is a public seam: createPetRegistry and createPetSegment are already exported so a third party can ship an animal without forking a file, and the shipped roster is already seven · bubba plus six hand-drawn sprites, each one file and two lines in an index. docs/PETS.md is the whole contribution procedure, because a seam nobody can find is not a seam.

typescript · node.js · cli · claude code · zero deps · vitest · ansi · unicode · developer tools

LIVE · MIT · npx ccline-cli install · npmjs.com