Two aligned rows with muted bars and bright values: model, context, effort and session time, then git branch (or a muted no-git chip), weekly limit with reset countdown, cost and lines changed. Themed ccstatusline version at github.com/hasuwini77/ccstatusline-nocturne

4 installs

Before you install

  • This version was reviewed before it was listed on Sep 16, 2026
  • Doesn't ask to connect to the internet
  • The chip arrows and branch icon are private-use glyphs (\ue0b0, \uf418), so use a Nerd Font.
  • On Claude Code versions without context_window, it estimates context from the transcript file.
  • Bars keep one color however full they get: context at 91% looks the same yellow as at 22%.
Not showing after you paste? Fix it

Preview

Clean repo
◆ Opus 4.8 ▏ ctx ▓▓░░░░░░░░ 22% ▏ high ▏ 10m main ▏ week ▓░░░░░░░░░ 7% ▏ 2d0h ▏ $0.41 ▏ +128/-34
New session
◆ Opus 4.8 ▏ ctx — ▏ high ▏ 10m main ▏ week — ▏ — ▏ $0.00
Dirty branch
◆ Sonnet 4.6 ▏ ctx ▓▓▓▓▓░░░░░ 48% ▏ medium ▏ 10m feat/auth ▏ week ▓▓░░░░░░░░ 18% ▏ 2d21h ▏ $0.41 ▏ +128/-34
Near-full
◆ Opus 4.8 ▏ ctx ▓▓▓▓▓▓▓▓▓░ 91% ▏ max ▏ 10m main ▏ week ▓▓▓▓▓▓░░░░ 61% ▏ 19h59m ▏ $4.12 ▏ +128/-34 ▏ Explanatory
1M context
◆ Fable 5 ▏ ctx ▓▓▓▓▓▓░░░░ 64% ▏ xhigh ▏ 10m main ▏ week ▓▓▓▓▓▓▓▓░░ 80% ▏ 4d23h ▏ $0.41 ▏ +128/-34
Post-compact
◆ Haiku 4.5 ▏ ctx — ▏ — ▏ 10m main ▏ 5h ▓▓▓▓▓░░░░░ 52% ▏ 4h01m ▏ $0.41 ▏ +128/-34
Worktree
◆ Opus 4.8 ▏ ctx ▓▓▓▓░░░░░░ 37% ▏ low ▏ 10m worktree-feature ▏ week ▓▓░░░░░░░░ 20% ▏ 1d6h ▏ $0.41 ▏ +128/-34
Non-git
◆ Opus 4.8 ▏ ctx ▓▓░░░░░░░░ 22% ▏ high ▏ 10m no git ▏ week — ▏ — ▏ $0.41 ▏ +128/-34

In Claude Desktop

Noctu in Claude Desktop

Recorded with statusline-anywhere.

What it does

Two aligned rows of chips and meters: model, context bar, effort and session time on top, then git branch, weekly limit with its reset countdown, cost and lines changed.

Written in
Python
Network access
None requested
Works in
Terminal

How it works

  1. Reads model, context_window, effort, cost, line counts and rate_limits from stdin.
  2. Runs git rev-parse for the branch chip, or shows a grey no git chip outside a repo.
  3. Uses the 7-day window, switching to the 5-hour one when that is the only window reported.
  4. Missing values show as a dim dash, and a non-default output style is added at the end.

Source

#!/usr/bin/env python3
"""Noctu - a two-row status line for Claude Code, no dependencies, no network.

Reads Claude Code's session JSON on stdin and prints two rows: a model chip
with the context meter, effort and session time, then a branch chip with the
weekly limit, its reset, cost and lines changed.

The source is ASCII-only on purpose: every glyph is a unicode escape, so the file
survives any copy-paste, and output is written as UTF-8 bytes whatever the
console's encoding.

The design rule: the bar recedes, the number speaks. Meters are a muted bar
plus a bright value, and only the model and the branch get filled badges,
because only those two are identity. The columns are aligned by measuring
each row rather than padding to a guess, so they hold with any branch name.
"""
import json
import os
import subprocess
import sys
import time

# palette (hex -> truecolor SGR)
VIOLET, VIOLET_DK = '7C5CFF', '5B3FD1'
TEAL, TEAL_DK = '2E9E7A', '1E6B52'
# outside a repository: the divider grey, sunk toward the background
SLATE, SLATE_DK, SLATE_INK, SLATE_GLYPH = '3C414B', '262A32', '979CA8', '727681'
YELLOW, YELLOW_BAR = 'F0C755', '6B5A28'
PURPLE, PURPLE_BAR, PURPLE_DIM = 'B69CFF', '463A6B', '8B79C9'
ORANGE, ORANGE_BAR, ORANGE_DIM = 'E8903C', '7A4A1E', 'C97B3C'
GREEN, BLUE, DIM, INK, WHITE = '7CE38B', '5B9BE8', '79808E', '0B0E14', 'FFFFFF'
EFFORT = '3E8E5A'
BAR_FULL, BAR_EMPTY, ARROW, DIVIDER, BRANCH_ICON, DIAMOND = '\u2593', '\u2591', '\ue0b0', '\u258f', '\uf418', '\u25c6'
BAR_WIDTH = 10


def fg(h):
    return '\x1b[38;2;%d;%d;%dm' % tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))


def bg(h):
    return '\x1b[48;2;%d;%d;%dm' % tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))


RESET = '\x1b[0m'


def chip(icon, label, body, icon_bg, ink=INK, glyph=WHITE, bold=True):
    """icon compartment + label + arrow cap - the badge that anchors each row"""
    return (bg(icon_bg) + fg(glyph) + ' ' + icon + ' ' + RESET +
            bg(body) + fg(ink) + ('\x1b[1m ' if bold else ' ') + label + ' ' + RESET +
            fg(body) + ARROW + RESET)


def meter(pct, bar_colour, value_colour, label):
    filled = round(max(0.0, min(100.0, pct)) / 100 * BAR_WIDTH)
    bar = fg(bar_colour) + BAR_FULL * filled + BAR_EMPTY * (BAR_WIDTH - filled) + RESET
    return (fg(value_colour) + label + RESET + ' ' + bar + ' ' +
            fg(value_colour) + '%d%%' % round(pct) + RESET)


def dim_meter(label):
    return fg(DIM) + label + ' \u2014' + RESET


def until(epoch):
    """Compact countdown to a unix time: 2d1h, 3h07m, 18m."""
    try:
        left = max(0, int(float(epoch) - time.time()))
    except (TypeError, ValueError):
        return None
    days, rem = divmod(left, 86400)
    hours, mins = rem // 3600, rem % 3600 // 60
    if days:
        return '%dd%dh' % (days, hours)
    return '%dh%02dm' % (hours, mins) if hours else '%dm' % mins


def limit_meter(limits):
    """The weekly window, or the five-hour one when that's all that has reported."""
    limits = limits if isinstance(limits, dict) else {}
    for key, label, bar, value, timer in (('seven_day', 'week', PURPLE_BAR, PURPLE, PURPLE_DIM),
                                          ('five_hour', '5h  ', ORANGE_BAR, ORANGE, ORANGE_DIM)):
        window = limits.get(key)
        if isinstance(window, dict) and window.get('used_percentage') is not None:
            left = until(window.get('resets_at'))
            return (meter(window['used_percentage'], bar, value, label),
                    fg(timer) + left + RESET if left else fg(DIM) + '\u2014' + RESET)
    return dim_meter('week'), fg(DIM) + '\u2014' + RESET


def context_percent(transcript, window):
    """Token usage of the most recent assistant turn, as a share of the window."""
    if not transcript or not os.path.exists(transcript):
        return None
    used = None
    try:
        with open(transcript, encoding='utf-8', errors='replace') as fh:
            for line in fh:
                if '"usage"' not in line:
                    continue
                try:
                    msg = json.loads(line).get('message') or {}
                except ValueError:
                    continue
                u = msg.get('usage')
                if isinstance(u, dict):
                    used = (u.get('input_tokens', 0) + u.get('output_tokens', 0) +
                            u.get('cache_read_input_tokens', 0) +
                            u.get('cache_creation_input_tokens', 0))
    except OSError:
        return None
    return None if used is None else min(100.0, used / window * 100)


def git_branch(cwd):
    try:
        out = subprocess.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], cwd=cwd or None,
                             capture_output=True, text=True, timeout=1)
        name = out.stdout.strip()
        return name if out.returncode == 0 and name else None
    except (OSError, subprocess.SubprocessError):
        return None


def duration(ms):
    if not ms:
        return None
    mins = int(ms // 60000)
    return '%dh%02dm' % (mins // 60, mins % 60) if mins >= 60 else '%dm' % mins


def visible_len(text):
    out = i = 0
    while i < len(text):
        if text[i] == '\x1b':
            j = text.find('m', i)
            if j == -1:
                break
            i = j + 1
            continue
        out += 1
        i += 1
    return out


def main():
    try:
        data = json.load(sys.stdin)
    except ValueError:
        return
    model = (data.get('model') or {}).get('display_name') or 'Claude'
    model_id = (data.get('model') or {}).get('id') or ''
    workspace = data.get('workspace') or {}
    cwd = workspace.get('current_dir') or data.get('cwd') or os.getcwd()
    cost = data.get('cost') or {}

    # Claude Code reports context usage directly; the transcript estimate is
    # only a fallback for versions that predate context_window
    ctx = data.get('context_window')
    if isinstance(ctx, dict):
        pct = ctx.get('used_percentage')
    else:
        window = 1_000_000 if '[1m]' in model_id else 200_000
        pct = context_percent(data.get('transcript_path'), window)

    left = [chip(DIAMOND, model, VIOLET, VIOLET_DK)]
    branch = git_branch(cwd)
    right = [chip(BRANCH_ICON, branch, TEAL, TEAL_DK) if branch else
             chip(BRANCH_ICON, 'no git', SLATE, SLATE_DK, SLATE_INK, SLATE_GLYPH, bold=False)]

    row1 = [meter(pct, YELLOW_BAR, YELLOW, 'ctx ') if pct is not None
            else dim_meter('ctx ')]
    level = (data.get('effort') or {}).get('level')
    row1.append(fg(EFFORT) + level + RESET if level else fg(DIM) + '\u2014' + RESET)
    took = duration(cost.get('total_duration_ms'))
    row1.append(fg(BLUE) + took + RESET if took else fg(DIM) + '\u2014' + RESET)

    row2 = list(limit_meter(data.get('rate_limits')))
    spent = cost.get('total_cost_usd')
    if spent is not None:
        row2.append(fg(GREEN) + '$%.2f' % spent + RESET)
    added, removed = cost.get('total_lines_added'), cost.get('total_lines_removed')
    if added or removed:
        row2.append(fg(GREEN) + '+%d' % (added or 0) + RESET + fg(DIM) + '/' + RESET +
                    fg('FF6B6B') + '-%d' % (removed or 0) + RESET)
    style = (data.get('output_style') or {}).get('name')
    if style and style != 'default':
        row2.append(fg(DIM) + style + RESET)

    # every column lines up: pad each cell to the widest one above or below it
    rows = [[left[0]] + row1, [right[0]] + row2]
    widths = {}
    for cells in rows:
        for i, cell in enumerate(cells[:-1]):
            widths[i] = max(widths.get(i, 0), visible_len(cell))
    sep = ' ' + fg(DIM) + DIVIDER + RESET + ' '
    out = [sep.join(cell + ' ' * (widths.get(i, 0) - visible_len(cell)) if i < len(cells) - 1 else cell
                    for i, cell in enumerate(cells))
           for cells in rows]
    sys.stdout.buffer.write('\n'.join(out).encode('utf-8'))

if __name__ == '__main__':
    main()

More status lines

  • 234 installs
╭─ CONTEXT ────────────────────────────────────────────╮ │ ████▊░░░░░░░░░░░░░░░░░ 22% of 200K · GOOD │ ├─ STATS ──────────────────────────────────────────────┤ │ ↓ 44.0K ↑ 1.4K │ $0.41 │ ⏱ 10m 12s │ +128/-34 │ ├─ REPO ───────────────────────────────────────────────┤ │ ◆ Opus 4.8 · main · 16:21 │ ╰──────────────────────────────────────────────────────╯
  • 103 installs
Opus 4.8 [high] ~/app main 16:20:55 | ⛁ ██░░░░░░░░ 22% | 5h ●◔○○○ 26% ↻2h7m | 7d ◔○○○○○○ 7% ↻2d1h │ $0.41 ⏱ 10m
  • 81 installs
████▊░░░░░░░░░░░░░░░░░ 22% of 200K · GOOD │ ↓ 44.0K ↑ 1.4K │ $0.41 │ ⏱ 10m 12s │ +128/-34 │ 5h 26% ↻2h06m │ 7d 7% ↻2d0h │ ◆ Opus 5.5 · main · 03:53
  • 52 installs
Opus 4.8 high ✦ │ ctx 22% │ app (main) │ PR #1287 ● │ $0.41 · 10m +128 -34 Session ● ● ● ● ● ● ● ● ○ ○ ○ 74% left Resets in 2h 6m Weekly ● ● ● ● ● ● ● ● ● ● ○ 93% left Resets in 2d 0h Fable ● ● ● ● ● ● ● ● ● ○ ○ 85% left Resets in 2d 0h ◐ Read:index.ts ✓ Read×1 ✓ Bash×1 ✓ Edit×1 ✓ Grep×1 ◐ code-reviewer Review the change for regressions
Model: Opus 4.8 | Thinking: high | Context: [▰▰▱▱▱▱▱▱▱▱] 45k/200k (22%) | cwd: ~/app | ⎇ main | (+0,-0) Cost: $0.41 | Block: 2hr 6m | Weekly: 7.0% | Session: 10m | In: 916.7 t/s | Out: 29.2 t/s | Cached: 2k | Session: [▰▱▱▱▱] 26.0% Mem: 0.8G/1.0G | (*゚∀゚*) | Session ID: b8e1c0d2-4a6f-4e2a-9c1b-3f5d7a9e2c10
app · main · Opus 4.8 · high ctx 22% 44k · 5h 26% ↻2h6m · 7d 7% ↻2d0h · cost $0.41 · msg $0.757

Related mods

Deck in Claude Desktop

A cockpit for Claude Code: plan bars and subagent strips, context, usage limits with reset countdowns and burn rates, spend, the model route and a pet. Six bar styles, three collapsed looks. CLI and Desktop.

Token Weather Usage in Claude Desktop

One line above the prompt: context tokens with their weather, a bar per recent prompt, your 5-hour and 7-day limits against the time elapsed, the prompt cache with its time left, the session cost with the last prompt's share in dollars and in points of the 5-hour limit, and the subagents running. Labels in English, French or Russian. After Token Weather (Anthropic, Apache-2.0) and the ideas of usage-meter (HolyGrail) and prompt-cache-control (Daniel San).

Flightdeck in Claude Desktop

Flightdeck: a live agent dashboard for Claude Code. Main model vitals, an on-call architect, every permission check, subagent cards and swimlanes, a turn receipt and a session log, all from real session events