Sprag

Claude Code statusline: cache hit rate, cache TTL countdown, 5-hour and 7-day rate limits

Reference for the Sprag statusline for Claude Code: prompt cache hit rate, cache expiry countdown, context usage, 5-hour and 7-day rate-limit windows, monthly spend, and the warning chips that lead the line when something is wrong.

Reading the statusline

Once the savings ledger has entries it renders as two rows β€” routing savings on row 1, diagnostics on row 2.

πŸ”€ Routing saved $2.09  |  fableβ†’sonnet Γ—1 $0.72 Β· opusβ†’haiku Γ—1 $0.57
⚠ Ctx 500k+ Β· πŸ€– Opus 5.5 Β· 🧠 Cache hit 98.8% Β· ⏳ Cache expires 59:46 Β· ✦ current β–°β–°β–°β–°β–°β–°β–°β–±β–±β–±β–±β–± 62% πŸ”„ 21:33 Β· πŸ“… weekly β–°β–°β–°β–°β–°β–±β–±β–±β–±β–±β–±β–± 38% πŸ”„ Tue 19:33 Β· πŸ“¦ Ctx 47% of 1M Β· πŸ’° Cache saved $1.0K Β· last 1d

With an empty ledger (no measured delegation yet) row 1 is not drawn and the layout stays single-line. If your build renders only the first row (some macOS Claude Code versions), pass --single-line.

SegmentMeaning
πŸ”€ row 1Lifetime routing savings + the model moves behind them. The breakdown sums exactly to the total, model names keep only the family (opusβ†’haiku). Full audit: route-scan savings
πŸ“„ row 2Lifetime doc2md conversion savings with a per-format breakdown. Whichever of routing/conversion saved more takes row 1
since 10/9After sprag saved reset the routing and doc2md counters count only from the reset, and the figure says so: Routing saved $3.20 since 10/9. Compact icon mode shortens it to πŸ”€ $3.20 (10/9~). With a reset on record a zero is shown too, so "just reset" is distinguishable from "feature off": Routing saved $0.00 since 10/9, and Doc2md saved $0 since 10/9 for doc2md, which drops the cents because a freshly reset counter has nothing to measure to the cent. A counter that was never reset and has nothing hides itself as before. The date is local, and carries the year only when it is not this year. Routing's zero gets its own row 1; doc2md's zero stays an inline chip, since a whole row for "$0" would be all frame
πŸ“Š Model used ● Opus 90% ● Sonnet 10%What the spend was made of over the same period as the routing counter (since the reset mark, or the start of the month when nothing was reset, capped at 90 days), by cost and by model family. A segment of its own after the savings and their breakdown, so a share is never read as another saving; a family that spent money but rounds to 0% shows as <1%. Narrow mode draws β—©. Only beside a savings figure, and sprag mode no-share hides it. What the delegated runs themselves cost (spent) is no longer on the line: between the saving and its date it read as one more saving, so sprag saved reports it instead, with the full share table
scan 3dDim, beside the routing figure, only when the last route-scan is more than 3 days old. The routing ledger is filled by that scan, so an old scan means the figure may be missing recent delegations. Verbose spells it last scan 3d ago. Absent while the scan is recent, and when no scan cache exists
πŸ”€ this session 3 (haiku 2 Β· sonnet 1)The subagents this session has started, by model family. Counts every subagent (Explore, general-purpose, your own), not only runs a routing rule sent, and shows nothing when the session ran none. The session is found from the transcript path and session id in Claude Code's payload, and the runs come from the transcripts the statusline already parses, so it adds no file reads. Runs are limited to the analysis window ((last 1d) by default), so a session older than the window shows only its recent runs. A model id the family table does not know is shown as it is, shortened, rather than folded into another family. A gray spent $0.42 follows: what those runs cost, each priced at its own model by the same path the session's own cost takes (a gateway session from the gateway's table). Runs the gateway cannot price are counted as +1 unpriced rather than read as free, and n/a means none could be priced. Verbose: πŸ”€ Subagents this session 3 (…)
πŸ€–Active model, as Claude Code names it. When the payload carries only a model id, the chip shortens it to family and version (claude-fable-5-1 β†’ Fable 5.1, us.anthropic.claude-3-5-sonnet-20241022-v2:0 β†’ Sonnet 3.5), dropping gateway prefixes and date stamps. An id whose family is not known is shown as it is
πŸ”¬ highEffort level the session is running at (/effort), in Claude Code's own colors for those levels, so the chip and the picker that set it agree: yellow at low (warning), green at medium (success), blue at high (permission), purple at xhigh and ultracode (autoAccept / effortUltra), and the rainbow spread across the word at max. Two of those are animated in the app β€” xhigh shimmers and max cycles β€” and a statusline cannot animate (it re-runs on a seconds-long interval, each run a fresh process), so both render as their still form. That leaves xhigh and ultracode sharing a hue, which is the app's own doing; the level word tells them apart. Colors follow your Claude Code theme setting, including the daltonized themes, and fall back to its ANSI themes on terminals without 24-bit color. Absent on models that carry no effort setting, and on Claude Code older than 2.1.276. /effort ultracode is xhigh plus dynamic workflow orchestration and reaches the statusline as plain xhigh, so the chip recovers the difference from the session transcript: the /effort command's own output, written the moment the command returns, and the ultracode system-reminder that lands when the next turn is assembled, whichever is later. So the chip follows the switch immediately, and a session that starts with ultracode already on shows xhigh until its first turn
πŸ…· 3/5Harness principle score (Harness mode). Drawn only when it has something to say: short of 5/5 (yellow), πŸ…· custom for a hand-edited block, or a red πŸ…·βš  warning. A full score with no warning is left off the line, since πŸ…· 5/5 on every line says only that there is nothing to do; --segments harness keeps it there
✍️ Korean styleOnly with --segments korean, and only while the Korean writing guidance is on (sprag korean on). A gray confirmation that the session-start injection is active, which is why it is no longer on the default line: a chip that never changes stops being read
🧠Cache hit rate over the analysis window (green at 85%+)
⏳Cache TTL countdown β€” send a message before expiry to keep the cache warm. Ticking while idle requires Claude Code v2.1.97+ (see If the countdown looks frozen)
✦ current / πŸ“… weekly5-hour / 7-day rate-limit window usage + reset time
πŸ“¦Context usage (e.g. Ctx 68% of 1M) β€” colored by fill. Current models default to 1M with no premium, but token volume itself drives per-turn cost and 5H/7D burn
πŸ’΅ Sep $42Estimated spend since 00:00 on the 1st of this month (local time). Summed per session with that session's model pricing, from this machine's session logs only. Hidden when a gateway budget chip is present, since that reports measured spend for every call the key served and two disagreeing dollar figures are worse than one (v3.35.0)
πŸ’³ budgetLiteLLM key budget gauge. When stdin carries no rate_limits, shows the key's spend against max_budget as πŸ’³ budget β–°β–°β–°β–±β–±β–±β–±β–±β–±β–±β–±β–± 26% $1.0K/$4.0K, with the time left on the billing cycle (v3.35.0, below)
πŸ’°Prompt-cache savings over the analysis window β€” a different number from row 1's πŸ”€, which is a lifetime total from the ledger
(last 1d)The analysis window, fused to the two chips it measures (🧠, πŸ’°) with no separator so it cannot read as the timeframe for the rest of the line. Change it with sprag mode 7d
v3.24.0The version you are running. Gray, at the tail, when it is the latest one
v3.24.0 βœ“6h agoVerbose only. How long ago the registry was last asked, on an up-to-date version. ago is part of the chip because a bare βœ“6h reads as "6 hours left", like the cache countdown. The time is the last check attempt: a check that failed (offline) stamps it too, so βœ“ means "asked", not "answered". Shown only when a latest version is on record, and not while checks are off (SPRAG_NO_UPDATE_CHECK). When a newer release exists the chip below is unchanged
rules 10 Β· ⚠1Only with --segments rules. How many delegation rules apply in this project (global ones plus this project's, minus any switched off), and how many of those are flagged for review. ⚠1 is the same review status that raises the πŸ…· rule-health warning, and is left off when none are flagged. Hidden when there are no rules. Opt-in because verbose is the default and the ⚠ count repeats the πŸ…· rule-health warning
⬆ v3.24.0 β†’ 3.25.0A newer release exists. Actionable, so it moves to the front of the line (Update notifications)

When something is wrong, a warning chip leads the line:

🚨 5H β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–± 94% πŸ”„ 12:36 Β· πŸ€– Opus 4.8 Β· 🧠 Cache hit 72.1% Β· ⚠ Cache miss Β· πŸ“… weekly β–°β–±β–±β–±β–±β–±β–±β–±β–±β–±β–±β–± 12% πŸ”„ Sun 14:26 Β· πŸ“¦ Ctx 200k Β· last 1d

When more than one πŸ…· warning is due, the chip names the highest in precedence order and counts the rest: πŸ…·βš  PEV-skip +2. The order is ratchet? > no-evidence > PEV-skip > ratchet-unloaded > compact-window? > rule-health > route?, unchanged; the count only keeps a lower one from being invisible while a higher one is up.

Verbose is the default. New installs render verbose, so the verbose-only βœ“6h ago age above shows unless you opted out with sprag mode compact (sprag mode verbose turns it back on).

Chips β€” 🚨 5H/7D NN% (cap imminent) Β· ⚠ Ctx 500k+ (a single request actually exceeded 500k) Β· ⚠ Cache miss Β· ⚠ Input spike Β· ⚠ Output heavy Β· ⚠ Call surge Β· ⚠ Rebuild churn Β· ⚠ 5m TTL. When both windows cross 90% at once, the sooner-resetting one is promoted to 🚨 and the other stays visible as a red segment (v2.16.0+).

When a chip appears

Run the /claude-token-saver Skill inside Claude β€” or just say the chip wording ("5H cap is up", "cache miss") and it auto-activates. The Skill surfaces the root-cause code + step-by-step fix. When a cap is imminent, run sprag handoff to back up your work state to markdown and continue in a fresh session.

⬆ Update notifications

A statusline cannot open a dialog, and it re-renders every ~300ms, so it can never touch the network while drawing. The notification is therefore split in two:

The registry lookup runs at most once every 24h in a detached background process and only ever writes a cache file (update-check.json) β€” the same shape npm's update-notifier uses. When that lookup finds a newer version it also reads the release notes for it, since the registry carries no changelog of its own; that request is unauthenticated (60/hour, against once a day here) and its failure costs the notes, never the notice. A failed check still stamps its timestamp, so an offline machine backs off instead of retrying on every render. Turn checks off entirely with SPRAG_NO_UPDATE_CHECK=1 or NO_UPDATE_NOTIFIER.

Spike issue codes

CodeMeaning
LARGE_INPUT_PER_REQUESTsingle request > 200k input tokens β€” per-turn re-billing and cap burn spike
LOW_HIT_RATEcache hit rate < 50%
BUCKET_5M_DOMINANT> 70% of cache writes hit the 5m bucket
HIGH_OUTPUT_RATIOoutput/input > 0.15 (output is 5Γ— input price)
HIGH_REQUEST_COUNTsession made 3Γ—+ your median (tool loop?)
FREQUENT_CACHE_REBUILDcache_creation > cache_read

Remediation commands are OS-aware (~/.zshrc for macOS/Linux/WSL, setx for Windows).

Label modes

The chips come in three styles. Emoji is the default; the other two exist because some terminals cannot draw emoji without breaking the line.

ModeLooks likeSelect with
icon🧠 Cache hit 98.8% Β· πŸ“¦ Ctx 47% of 1M Β· πŸ”¬ highdefault, or sprag mode icon
narrowβ—‰ Cache hit 98.8% Β· β—§ Ctx 47% of 1M Β· β–² highautomatic in JetBrains IDEs, or sprag mode narrow / --narrow
textCache hit 98.8% Β· Ctx 47% of 1M Β· Effort highsprag mode text / --text

Why JetBrains IDEs get narrow glyphs

Inside a JetBrains IDE terminal (IntelliJ, PyCharm, WebStorm, DataGrip β€” anything that sets TERMINAL_EMULATOR=JetBrains-JediTerm) the emoji set garbles. The symptoms look like the statusline is computing wrong numbers:

⏳ Cache expires 4:545      (should read 4:54)
πŸ“¦ Ctx 330% of 1M           (should read 33%)
πŸ’³ budget $1.0K/$4.:0K      (should read $4.0K)

Nothing is miscomputed. The countdown formatter cannot emit a value without a colon, and percentages are clamped to 0–100, so those strings are impossible to produce. What you are seeing is leftover characters from the previous frame.

The cause is font coverage. JetBrains Mono, the IDE default, has no glyphs for the emoji, so the terminal draws them through a fallback font whose advance width does not match the cell grid. Static text survives that, but a chip whose value changes every second repaints partially, and the mismatch stays on screen as debris. Dragging a selection across the line forces a full repaint and it reads correctly again β€” the buffer was always right, only the painting was off.

Every glyph in the narrow set is a character JetBrains Mono actually ships. That is also how the cause was found: the gauge was the one part of the line that never garbled, and its block characters were the only ones already in the font.

The gauge itself follows the same rule. Its ticks are β–°β–± normally, but JetBrains Mono has neither character, so narrow mode draws the same bar as β– β–‘, which it does have. Both read as a row of twelve ticks; only the shape of one tick differs.

If your terminal garbles the line too

Any terminal whose font lacks emoji glyphs can show the same debris. Only JetBrains IDEs are detected automatically, because a program cannot ask a terminal which font it is using. Switch manually:

sprag mode narrow    # keep a glyph on every chip, without the emoji
sprag mode text      # drop the glyphs entirely

To keep emoji inside a JetBrains IDE anyway, accepting the garbling:

sprag mode icon-force      # persisted
SPRAG_ICON=force           # one-off, e.g. while capturing a screenshot

sprag mode reports what actually renders and which rule decided:

labels:  icon
renders: narrow (IntelliJ: emoji garble in the IDE font. `sprag mode icon-force` keeps them anyway)

More docs