The session's cost in the one line that is always on screen: a compiled renderer that reads the harness payload and prints the model, the context window, the…

One status line for Claude Code that shows the model, the context window, the rate limits and the git state, for the person who watches what a session costs.
Claude Code's default status line shows little beyond the model, so the human cannot see what the session costs until it is too late to change course. qsm-statusline replaces it with one line that is always on screen: model, context window used, five-hour and weekly rate limits, and git state. It takes the stance that the human decides the pacing and can only do so with the numbers in front of them. It refuses to be a dashboard or a notification system. When the harness shows these numbers itself, this plugin can go.
| Kind | Name | What it does |
|---|---|---|
| Skill | setup-statusline | Installs, updates, fixes or removes the status line, and writes the settings key with your go |
| CLI | qsm-statusline | 3 verbs (render, refresh, doctor), see USAGE.md |
The line shows the model, the context window used, the five-hour and weekly rate limits with reset times, and the git state. A source that is not there drops its segment. The render never waits on git.
Opus 5 | 370k/1m
context: ██████░░░░░░░░░ 37% | git rewrite: +2 ~1 ?3
session: ██████░░░░ 62% resets in 2h | week: ██░░░░░░░░ 18% resets in 3d 11h
claude plugin install qsm-statusline@qsm-marketplace
bun plugins/qsm-statusline/scripts/install.ts
The install script compiles qsm-statusline and links it into ~/.local/bin. It does not touch your settings. Add this key yourself, then restart Claude Code:
{ "statusLine": { "type": "command", "command": "qsm-statusline render", "padding": 1 } }
More in INSTALL.md.
Ask: "set up the statusline" or "my statusline is broken". The skill fires and does the wiring. To check it yourself:
qsm-statusline doctor
echo '{}' | qsm-statusline render --no-color
To remove it, delete the statusLine key and restart Claude Code.
hooks/register.tsx 10 lines1// qsm-statusline's function-hooks module.
2import type { On } from "claude-code";
3import { registerNoun } from "./noun.generated.ts";
4
5export function register(on: On): void {
6 // Adds $.statusline: one method per qsm-statusline verb, generated by lib/generate-noun.ts. Never
7 // hand-edit noun.generated.ts or types/index.d.ts.
8 registerNoun(on);
9}
10hooks/noun.generated.ts 127 lines1// Generated by lib/generate-noun.ts from qsm.json. Do not hand-edit; run
2// `bun lib/generate-noun.ts plugins/<name>` again after changing qsm.json.
3import type { Caught, EngineInterface, On } from "claude-code";
4import type { Statusline } from "../types/index.d.ts";
5
6/** How one option becomes argv: a positional value (`flag` null), a bare
7 * boolean flag, or a flag and its value; a list repeats it per element. */
8export interface VerbArg {
9 readonly name: string;
10 readonly flag: string | null;
11 readonly boolean?: true;
12}
13
14export interface VerbRow {
15 readonly words: readonly string[];
16 readonly args: readonly VerbArg[];
17 readonly passthrough?: true;
18}
19
20export const verbs: Readonly<Record<string, VerbRow>> = {
21 render: { words: ["qsm-statusline","render"], args: [{ name: "cwd", flag: "--cwd" }, { name: "noColor", flag: "--no-color", boolean: true }] },
22 refresh: { words: ["qsm-statusline","refresh"], args: [{ name: "cwd", flag: "--cwd" }] },
23 doctor: { words: ["qsm-statusline","doctor"], args: [{ name: "settings", flag: "--settings" }] },
24};
25
26/** One verb's argv: its words, each option in declaration order, the
27 * passthrough rest after `--`, and `--json` last. */
28export function argvOf(row: VerbRow, options: Readonly<Record<string, unknown>> = {}): string[] {
29 const argv = [...row.words];
30 for (const arg of row.args) {
31 const value = options[arg.name];
32 const values = Array.isArray(value) ? value : [value];
33 for (const one of values) {
34 if (one === undefined || one === false) {
35 continue;
36 }
37 argv.push(...(arg.flag === null ? [] : [arg.flag]), ...(arg.boolean ? [] : [String(one)]));
38 }
39 }
40 const rest = options.rest;
41 if (row.passthrough && Array.isArray(rest)) {
42 argv.push("--", ...rest.map(String));
43 }
44 argv.push("--json");
45 return argv;
46}
47
48/** The binary prints one outcome envelope on stdout under --json:
49 * { code, exit, reason, message, warnings, data }. Data on ok; otherwise the
50 * code and message become the error. A stale or misbehaving binary that
51 * prints something else (not JSON, or JSON with no numeric `exit`) throws
52 * loud here instead of a raw JSON.parse SyntaxError or a silent `undefined`. */
53export function unwrap(
54 argv: readonly string[],
55 result: { readonly exitCode: number; readonly stdout: string; readonly stderr: string },
56 label = argv.slice(0, 2).join(" "),
57): unknown {
58 const text = result.stdout.trim();
59 const stderr = result.stderr.trim().slice(0, 200);
60 const cause = `(exit ${result.exitCode}): ${stderr.length > 0 ? stderr : "no stderr"}`;
61 if (text.length === 0) {
62 throw new Error(`${argv.join(" ")}: printed no envelope ${cause}`);
63 }
64 let parsed: unknown;
65 try {
66 parsed = JSON.parse(text);
67 } catch {
68 parsed = undefined;
69 }
70 if (typeof parsed !== "object" || parsed === null || typeof (parsed as { exit?: unknown }).exit !== "number") {
71 throw new Error(`${argv.join(" ")}: stdout is not an outcome envelope ${cause}: ${text.slice(0, 200)}`);
72 }
73 const envelope = parsed as { code: string; exit: number; message: string; data: unknown };
74 if (envelope.exit !== 0) {
75 throw new Error(`${label}: ${envelope.code}: ${envelope.message}`);
76 }
77 return envelope.data;
78}
79
80/** What every `.catch` does first: a toast and a log for the person. The toast says `fixedReason`
81 * when the cause may carry something the toast must not show, else the cause; the cause goes to
82 * the log and comes back for the caller's own policy. */
83export function failure(
84 $: EngineInterface,
85 event: string,
86 error: Caught["error"],
87 fixedReason?: string,
88): string {
89 const said = error.message ?? error.kind;
90 $.ui.toast(`qsm-statusline ERROR: ${event}: ${(fixedReason ?? said).slice(0, 120)}`);
91 $.ui.log(`${event} failed (${error.kind}): ${said}`);
92 return said;
93}
94
95/** Adds $.statusline to the engine. Call once from the plugin's own
96 * `register(on, options)` — never call the engine.create hook's own
97 * `next(e)` result from anywhere but here. */
98export function registerNoun(on: On): void {
99 on("engine.create", async (_$, e, next) => {
100 const beneath = await next(e);
101 const run = async (row: VerbRow, options?: Readonly<Record<string, unknown>>): Promise<unknown> => {
102 const argv = argvOf(row, options);
103 let result: Awaited<ReturnType<typeof beneath.process.run>>;
104 try {
105 result = await beneath.process.run(argv);
106 } catch (error) {
107 // A spawn in a deleted directory fails as ENOENT naming the binary; name the directory. A
108 // check that cannot run leaves the spawn's own error as the cause.
109 const gone = await beneath.session
110 .cwd()
111 .then(async (cwd) => ((await beneath.fs.exists(cwd)) ? null : cwd))
112 .catch(() => null);
113 if (gone !== null) {
114 throw new Error(`${row.words.join(" ")}: the session's working directory ${gone} no longer exists; cd to one that does`);
115 }
116 throw error;
117 }
118 return unwrap(argv, result, row.words.join(" "));
119 };
120 const methods = Object.entries(verbs).map(([method, row]) => [
121 method,
122 (options?: Readonly<Record<string, unknown>>) => run(row, options),
123 ]);
124 return { ...beneath, statusline: Object.fromEntries(methods) as Statusline };
125 });
126}
127types/index.d.ts 22 lines1// Generated by lib/generate-noun.ts from qsm.json. Do not hand-edit; run
2// `bun lib/generate-noun.ts plugins/<name>` again after changing qsm.json.
3
4export type Statusline = {
5 render(options: {
6 cwd?: string;
7 noColor?: boolean;
8 }): Promise<unknown>;
9 refresh(options: {
10 cwd?: string;
11 }): Promise<unknown>;
12 doctor(options: {
13 settings?: string;
14 }): Promise<unknown>;
15};
16
17declare module 'claude-code' {
18 interface EngineInterface {
19 statusline: Statusline;
20 }
21}
22