# SmartStack status line

Deployed by `ss install` to `~/.claude/scripts/statusline/index.js` and wired into
`~/.claude/settings.json`. Renders a 3-line Claude Code status line:

```
CTX <bar> N% │ 5h <bar> N% (Nh) │ 7d <bar> N% (Nj) │ <Model> <bar> N%
<user> │ <model>·<effort> │ <dir> │ ⎇ <branch>
+added/-removed │ $cost
```

- **L1 — gauges**: context window + the 5-hour and 7-day (weekly) rate limits + any
  **per-model weekly quota** (e.g. `Fable`), each with a 10-char bar. Context colors by
  usage (<30 green / 30-40 orange / >40 red).
  The rate-limit gauges color by **pace**: green while consumption% stays below the %
  of the window still remaining, orange once consumption catches up to it, red when it
  overshoots by 20 points. 5h and 7d show the time until reset as a bare value in parens —
  hours for 5h (`Nh`), days for the weekly 7d (`Nj`), no "reset" label. The model gauges
  share the 7d window, so they reuse its pace coloring and carry no reset of their own.
- **L2 — session**: the signed-in Claude account e-mail (read from `~/.claude.json`,
  **not** the stdin payload — which carries no identity field), then the model with
  the reasoning **effort** tier right after it (e.g. `Opus·high` — green for
  low/medium, orange for high/xhigh, red for max), working dir, git branch.
- **L3 — cost**: lines changed (`+added/-removed`) and the session cost.
- Pure Node, **zero dependencies** (runs on the Node that ships with the CLI).
- Reads the Claude Code session JSON from stdin (`context_window`, `rate_limits`,
  `cost`, `effort`, …), writes up to three lines to stdout. Any segment whose data is absent is
  dropped, so older Claude Code builds (no `rate_limits`) degrade cleanly.
- Reads the git branch directly from `.git/HEAD` (no `git` subprocess).
- Reads the signed-in account e-mail from `~/.claude.json` (`oauthAccount.emailAddress`)
  — regex-extracted, not full-parsed, since that file can hold ~1 MB of history.
- Honors `NO_COLOR`.

## Per-model quotas (Fable & co)

Claude Code's stdin payload projects **only two** rate-limit windows — `five_hour` and
`seven_day`. The per-model weekly buckets that `/usage` renders as *"Current week (Fable)"*
never reach the status line, so this script sources them itself:

- a **background maintenance process** (`node index.js --maintenance --refresh`, started
  *after* the render is written — see *Process hygiene* below for how) calls
  `GET https://api.anthropic.com/api/oauth/usage` and keeps every `limits[]` entry of kind
  `weekly_scoped` carrying a `scope.model.display_name`;
- the result lands in `<claudeDir>/cache/smartstack-usage.json` (atomic write), and the
  render path only ever reads that cache — **it never touches the network**, so the
  refresh loop stays as fast as before. A fresh quota shows up on the next tick.
- TTL 60 s, a 20 s lock so concurrent renders start a single refresher, 10 min backoff
  after a failure, and the gauge is dropped once the cache is older than 15 min.
- The OAuth token is read (never refreshed, never logged) from
  `<claudeDir>/.credentials.json` — Claude Code itself keeps that file current.

Everything degrades to *"no gauge"*: no credentials file (macOS keychain, API key,
Bedrock/Vertex), expired token, 401/403, offline, no model-scoped quota on the plan.
Set **`SMARTSTACK_STATUSLINE_NO_NET=1`** to disable the feature outright — no fetch,
no gauge. `node index.js --refresh-usage` warms the cache by hand.

## Process hygiene on Windows (frozen processes)

On Windows, Claude Code runs the status line command through **Git Bash**
(`bash -c "node …/index.js"`), and on every refresh tick it **aborts** the previous
render if it is still running — `taskkill /PID <bash> /T /F` on the whole tree. Two
spawners in that chain create their child `CREATE_SUSPENDED` and resume it afterwards:
Cygwin (for any native child — node.exe included) and libuv (for `detached: true`).
A parent killed between `CreateProcess` and `ResumeThread` leaves a child that has
**never run a single instruction**: 1 thread, 0 s CPU, no module loaded, alive for
ever, with a `conhost` and a `cygwin-console-helper` attached to it. At 2 s and five
sessions under load this piled up hundreds of `node.exe` per night.

What the script does about it:

- **No `detached` spawn on Windows.** Background work is launched through
  `cmd /d /s /c "start "" /b node …"`: cmd is a plain (non-suspended) libuv child, its
  `start` grandchild is a plain `CreateProcess` that silently breaks away from libuv's
  job and survives the render's exit. The render exits only once that launcher has
  exited (≤ 600 ms, ~40 ms idle). POSIX keeps the classic detached fork.
- **A sweep every 5 minutes per machine** (stamp file `<claudeDir>/cache/smartstack-sweep.stamp`,
  claimed by the render so a killed maintenance process never loops). It runs a
  PowerShell/CIM query and terminates ONLY:
  - `node.exe` whose command line names `statusline/index.js`, with exactly 1 thread,
    0 CPU, older than 60 s and no living parent — a process that provably never started
    (a live render has ≥ 7 threads within milliseconds);
  - `cygwin-console-helper.exe` whose parent is gone, older than 60 s (the invisible
    console Git Bash spawned for that native child). Their `conhost` exits by itself.
  Nothing else is ever touched. Opt out with **`SMARTSTACK_STATUSLINE_NO_SWEEP=1`**.
- **`refreshInterval: 5`** instead of 2 (see below): the abort — hence the window —
  becomes the exception instead of the rule.

On demand: `ss doctor` reports the frozen count, `ss doctor --fix` purges them,
`ss install` / `ss update` purge the backlog right after deploying. By hand:
`node ~/.claude/scripts/statusline/index.js --sweep-orphans [--dry-run]` prints the JSON
`{ candidates, killed, dryRun }`.

The `bash → node` hop itself is Claude Code's and Cygwin's; it cannot be changed from
here (the `statusLine` settings schema accepts neither an exec form nor a shell choice).

## settings.json wiring

`ss install` adds (with the path resolved to the real install location):

```json
{
  "statusLine": {
    "type": "command",
    "command": "node \"<claudeDir>/scripts/statusline/index.js\"",
    "padding": 0,
    "refreshInterval": 5
  }
}
```

`refreshInterval` re-runs the script every 5 s on top of Claude Code's event triggers
(new assistant message, `/compact`, permission-mode, vim). Without it, session state
that ISN'T a trigger — the reasoning **effort**, the rate-limit gauges, a branch a
background agent moved — stays stale on screen until the next assistant message.
It was 2 s until 5.21: Claude Code aborts a render still running when the next tick
fires, and under load a render regularly exceeded 2 s (see *Process hygiene*). An
installation that already points at this script is moved to the new cadence by the
next `ss install`/`ss update` (the value is part of the "unchanged" check).

It is set **only if you have no status line yet**. If a custom `statusLine` already
exists it is left untouched — run `ss install --force` to replace it (the previous
`settings.json` is backed up to `settings.backup-<timestamp>.json` first).

## Editing

Edit the source at `templates/scripts/statusline/index.js` in the CLI repo, then
re-run `ss install`. **Never** edit the deployed copy under `~/.claude/` — it is
overwritten on every install/upgrade. The behaviour is locked by
`src/lib/__tests__/statusline-script.test.ts` (`npm run test:cli`).
