Skip to content

Instantly share code, notes, and snippets.

@josmithiii
Last active August 3, 2026 22:18
Show Gist options
  • Select an option

  • Save josmithiii/72bb219437e881521e72028bf01bb99a to your computer and use it in GitHub Desktop.

Select an option

Save josmithiii/72bb219437e881521e72028bf01bb99a to your computer and use it in GitHub Desktop.
Claude Code status line: 1M-token context usage with color-coded percentage, plus weekly rate-limit usage (Pro/Max)

Claude Code status line -- context and plan usage

A custom status line for Claude Code that shows the model name and current effort level, percentage of the 1M-token context window consumed (color-coded green/yellow/red), exact token counts, the remaining 5-hour-session and weekly rate-limit percentages with time until each window resets (Pro/Max plans), the API-equivalent session cost, the session ID, plus the familiar user@host:dir (branch) prefix.

Example output:

jos@laptop:myproj (main) Opus 5 xhigh | context used 31.9% - (318,911/1,000,000) - remaining: 5h 62% (2.2h) | weekly 69% (5.3d) | ~$12.88 if API

Rendered (the user@host:project prefix comes from the wrapper; everything from (main) onward is shown in the screenshot below):

status line screenshot

Install

  1. Drop both files in ~/.claude/:

    GIST=https://gist.githubusercontent.com/josmithiii/72bb219437e881521e72028bf01bb99a/raw
    curl -o ~/.claude/statusline.js          $GIST/statusline.js
    curl -o ~/.claude/statusline-wrapper.js  $GIST/statusline-wrapper.js
    chmod +x ~/.claude/statusline.js ~/.claude/statusline-wrapper.js
  2. Wire it up in ~/.claude/settings.json:

    {
      "statusLine": {
        "type": "command",
        "command": "~/.claude/statusline-wrapper.js",
        "padding": 0
      }
    }
  3. Start a new Claude Code session -- the bar appears at the bottom.

How it works

  • statusline-wrapper.js (a bash script despite the .js name -- Claude Code only cares that it's executable) prepends user@host:dir (branch) and then pipes the original stdin JSON into statusline.js.
  • The effort level after the model name (Opus 5 xhigh) is the effort.level field Claude Code pipes in on stdin -- one of low, medium, high, xhigh, tracking /effort live. The whole effort block is omitted for models that don't support effort levels (Sonnet 4.5, Haiku 4.5, Opus 4.0/4.1, Claude 3.x), and the status line then shows the bare model name. Note that ultracode reports as xhigh, its underlying effort level.
  • statusline.js walks the session transcript from the tail, finds the newest main-context assistant message with non-zero usage (skipping sidechains, synthetic messages, API errors, and "no response requested" turns), and reports input + output + cache_read + cache_creation as a percentage of the 1M-token window.
  • The remaining: 5h N% (…h) | weekly N% (…d) segments are read straight from the rate_limits.five_hour and rate_limits.seven_day blocks that Claude Code pipes into the status line on stdin. Each shows the percentage remaining in that window (100 minus used_percentage) followed by the time until the window resets; the green/yellow/red color still tracks usage, so the number turns red as it approaches 0% remaining. The fields only appear for Claude.ai Pro/Max subscribers, and only after the session's first API response -- before then each segment is silently omitted. See: https://code.claude.com/docs/en/statusline.md#rate-limit-usage
  • ~$1.23 if API is the cost.total_cost_usd field -- Claude Code's client-side estimate of the current session's spend. It prices tokens at standard per-token API rates with no knowledge of the Max subscription, so read it as "what this session would cost at pay-as-you-go API rates" -- the on-demand bill Max avoids. It is a running dollar total (not a percentage), shown in neutral cyan, so the green/yellow/red usage scale does not apply. It is a client-side estimate and "may differ from your actual bill." Note that Claude Code exposes no remaining-credit balance to the status line; this session-cost estimate is the closest available signal.

Tweaks

  • Change CONTEXT_WINDOW in statusline.js if you're on a non-1M model.
  • Adjust the color thresholds (90% red, 70% yellow) in color().
  • Recolor the effort label (bright blue, \x1b[94m) or drop it entirely by setting effort to "" near the top of statusline.js.
  • Other fields Claude Code puts on stdin but this script ignores: fast_mode, thinking.enabled, output_style.name, vim.mode, agent.name, session_name, pr, worktree, and a ready-made context_window block (used_percentage, remaining_percentage) that could replace the transcript walk on newer Claude Code versions.
  • Drop the wrapper and point statusLine.command straight at statusline.js if you don't want the user/host/branch prefix.

License

MIT

#!/bin/bash
input=$(cat)
# Your existing info
user_host="$(whoami)@$(hostname -s):$(basename "$(pwd)")"
branch=$(git branch --show-current 2>/dev/null | sed 's/.*/(&)/')
# New script (pipe same input)
context_info=$(echo "$input" | ~/.claude/statusline.js)
printf "%s %s %s" "$user_host" "$branch" "$context_info"
#!/usr/bin/env node
"use strict";
const fs = require("fs");
// --- input ---
const input = readJSON(0); // stdin
const sessionId = `\x1b[90m${String(input.session_id ?? "")}\x1b[0m`;
const transcript = input.transcript_path;
const model = input.model || {};
// Claude Code sends `effort: { level }` (low | medium | high | xhigh) only for
// models that support effort levels; the whole block is absent otherwise.
const effortLevel = String(input.effort?.level ?? "").trim();
const effort = effortLevel ? ` \x1b[94m${effortLevel}\x1b[0m` : "";
const name = `\x1b[95m${String(model.display_name ?? "").trim()}\x1b[0m${effort}`;
const CONTEXT_WINDOW = 1_000_000;
// --- helpers ---
function readJSON(fd) {
try {
return JSON.parse(fs.readFileSync(fd, "utf8"));
} catch {
return {};
}
}
function color(p) {
if (p >= 90) return "\x1b[31m"; // red
if (p >= 70) return "\x1b[33m"; // yellow
return "\x1b[32m"; // green
}
const comma = (n) =>
new Intl.NumberFormat("en-US").format(
Math.max(0, Math.floor(Number(n) || 0))
);
function usedTotal(u) {
return (
(u?.input_tokens ?? 0) +
(u?.output_tokens ?? 0) +
(u?.cache_read_input_tokens ?? 0) +
(u?.cache_creation_input_tokens ?? 0)
);
}
function syntheticModel(j) {
const m = String(j?.message?.model ?? "").toLowerCase();
return m === "<synthetic>" || m.includes("synthetic");
}
function assistantMessage(j) {
return j?.message?.role === "assistant";
}
function subContext(j) {
return j?.isSidechain === true;
}
function contentNoResponse(j) {
const c = j?.message?.content;
return (
Array.isArray(c) &&
c.some(
(x) =>
x &&
x.type === "text" &&
/no\s+response\s+requested/i.test(String(x.text))
)
);
}
function parseTs(j) {
const t = j?.timestamp;
const n = Date.parse(t);
return Number.isFinite(n) ? n : -Infinity;
}
// Find the newest main-context entry by timestamp (not file order)
function newestMainUsageByTimestamp() {
if (!transcript) return null;
let latestTs = -Infinity;
let latestUsage = null;
let lines;
try {
lines = fs.readFileSync(transcript, "utf8").split(/\r?\n/);
} catch {
return null;
}
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i].trim();
if (!line) continue;
let j;
try {
j = JSON.parse(line);
} catch {
continue;
}
const u = j.message?.usage;
if (
subContext(j) ||
syntheticModel(j) ||
j.isApiErrorMessage === true ||
usedTotal(u) === 0 ||
contentNoResponse(j) ||
!assistantMessage(j)
)
continue;
const ts = parseTs(j);
if (ts > latestTs) {
latestTs = ts;
latestUsage = u;
}
else if (ts == latestTs && usedTotal(u) > usedTotal(latestUsage)) {
latestUsage = u;
}
}
return latestUsage;
}
// --- rate-limit labels (Pro/Max; appear after first API response) ---
// Shows the percentage *remaining* (100 - used); color still tracks usage.
function rateLimitLabel(window, name, sep) {
const p = Number(window?.used_percentage);
if (!Number.isFinite(p)) return "";
const remaining = 100 - p;
let suffix = "";
const resetsAt = Number(window?.resets_at);
if (Number.isFinite(resetsAt)) {
const hrs = (resetsAt * 1000 - Date.now()) / 3_600_000;
if (hrs > 0) {
suffix = hrs >= 24 ? ` (${(hrs / 24).toFixed(1)}d)` : ` (${hrs.toFixed(1)}h)`;
}
}
return `${sep}${color(p)}${name} ${remaining.toFixed(0)}%${suffix}\x1b[0m`;
}
// --- session cost ---
// Claude Code exposes no remaining-credit balance to the status line, only
// this client-side estimate of the current session's spend (cost.total_cost_usd).
// It prices tokens at standard per-token API rates with no knowledge of the Max
// subscription, so it reads as "what this session would cost at pay-as-you-go API
// rates" -- i.e. the on-demand bill Max avoids. Shown in neutral cyan -- it is a
// running total, not a percentage, so the usage color scale does not apply.
function costLabel(sep) {
const c = Number(input?.cost?.total_cost_usd);
if (!Number.isFinite(c)) return "";
return `${sep}\x1b[36m~$${c.toFixed(2)} if API\x1b[0m`;
}
function rateLimitLabels() {
return (
rateLimitLabel(input?.rate_limits?.five_hour, "5h", " - remaining: ") +
rateLimitLabel(input?.rate_limits?.seven_day, "weekly", " | ") +
costLabel(" | ")
);
}
// --- compute/print ---
const usage = newestMainUsageByTimestamp();
if (!usage) {
console.log(
`${name} | \x1b[36mcontext window usage starts after your first question.\x1b[0m${rateLimitLabels()}\nsession: ${sessionId}`
);
process.exit(0);
}
const used = usedTotal(usage);
const pct = CONTEXT_WINDOW > 0 ? Math.round((used * 1000) / CONTEXT_WINDOW) / 10 : 0;
const usagePercentLabel = `${color(pct)}context used ${pct.toFixed(1)}%\x1b[0m`;
const usageCountLabel = `\x1b[33m(${comma(used)}/${comma(
CONTEXT_WINDOW
)})\x1b[0m`;
console.log(
`${name} | ${usagePercentLabel} - ${usageCountLabel}${rateLimitLabels()}\nsession: ${sessionId}`
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment