When the context window crosses your threshold, Claude gets one quiet note to reach a clean stopping point before auto-compaction, and you get a toast. /brake…

Two small, observe-first Claude Code mods (function-hook plugins) for people who run long sessions and keep getting surprised by the context window.
| Mod | What it does | Reach |
|---|---|---|
| context-receipt | After every turn, a one-line band above the prompt says how much the context grew and which tool results did it, e.g. ctx 61% +40k last turn · Read big.log ≈14k · Bash npm test ≈2k. /receipt lists the last turn and the session's biggest results. | Draws and remembers. No files, no network, no processes, no model calls. Never changes a tool call. |
| context-brake | When the context crosses your threshold (default 80%), Claude gets one short note with your next prompt: finish the current step, stop at a clean boundary, leave a handoff. You get a toast. Re-arms after /compact or /clear. | Adds one note beside one prompt per fill. Your prompt text is never edited. No files, no network, no processes, no model calls. |

The GIF is drawn by
scripts/make-demo.pyfrom band output produced by the real Claude Code runtime (claude plugin test, v2.1.301). The window chrome is synthetic; it is not a screen recording of a live session.
Context fill drives real decisions mid-session (keep going, delegate, compact, hand off), and the ambient indicator for it keeps disappearing:
There are already many good context meters in the community catalogue (token-weather, flightdeck, context-band, and others), and /context gives an exact category breakdown. These two mods cover what a percentage alone does not:
Read of a log file, so you know what to avoid next time.Both work in the terminal and the desktop app (the band uses the AbovePrompt component; the brake has no UI beyond a toast).
Requires Claude Code 2.1.287 or later (mods are on by default).
From GitHub (after this repo is published):
/plugin marketplace add jonirafael/claude-code-mods
/plugin install context-receipt@claude-code-mods
/plugin install context-brake@claude-code-mods
/reload-plugins
From a local clone:
claude --plugin-dir ./plugins/context-receipt --plugin-dir ./plugins/context-brake
# or
claude plugin marketplace add ./claude-code-mods
claude plugin install context-receipt@claude-code-mods --scope user
context-receipt
/receipt last turn and the biggest tool results this session
/receipt off|on hide or show the band
/receipt reset clear the history
Tool-result sizes are estimates (≈4 characters per token) of the text each tool returned to the model; the headline growth (+40k) is the real figure from $.session.usage(). Results under 50 tokens and subagent-internal tool calls are ignored (a subagent's final report still counts, as the Agent call). The band yields to surveys and passes through when there is nothing to show.
context-brake
/brake status: threshold, current fill, armed / sent / waiting
/brake 70 set the threshold (10–99), saved across sessions
/brake off disable
The note Claude receives (once per fill):
[context-brake] The context window is 83% full and auto-compaction is approaching. Finish the step you are on, then stop at a clean boundary instead of starting large new work. Before stopping, give a short handoff: goal, what is done, what is left, key files and decisions. This is a one-time note from a user-installed mod; the user may run /compact afterwards.
Plain ES modules, no build step, no dependencies:
plugins/<mod>/
├── .claude-plugin/plugin.json manifest (+ "types" contract for $.state)
├── hooks/hooks.json { "modules": ["./<mod>.mjs"] }
├── hooks/<mod>.mjs register(on): the hooks
├── hooks/core.mjs pure logic, unit tested
├── types/index.d.ts PluginState declarations
└── tests/*.test.ts run by `claude plugin test`
context-receipt hooks tool.call (observe: await next(e) then measure), turn.start/turn.complete (main loop only, !e.agentId), session.end, command.run, and ui.render{component=AbovePrompt}. History lives in $.state, so it survives hot reload, and reading state during render subscribes the band to redraws.context-brake hooks turn.complete to read $.session.usage(), and prompt.submit to forward the prompt with one extra entry in context (the prompt's list of side texts). The threshold is kept in $.store.npm run validate # claude plugin validate: manifest, hooks, $ calls, state contract
npm test # claude plugin test: unit + adapter tests against the real runtime
npm run demo # regenerate the GIF from runtime output
Status at v0.1.0, Claude Code 2.1.301 on Linux: validation passes for the marketplace and both plugins; 12 tests pass (8 context-receipt, 4 context-brake). The adapter tests drive the real mods runtime with stubbed session.usage, tool.call and prompt.submit. Not yet verified in a live, logged-in session (terminal or desktop). If something renders oddly, run claude --debug and please open an issue.
Neither mod reads files, runs processes, calls the network, or calls a model. context-receipt keeps labels such as Read session.ts or the first 28 characters of a Bash command in session state; it never stores tool output.
MIT © 2026 biqpro SH. Not affiliated with Anthropic.
hooks/context-brake.mjs 90 lines1// context-brake: one gentle, one-time nudge when the context window gets full.
2import { DEFAULT_THRESHOLD, note, parseThreshold, step } from "./core.mjs";
3
4const PERCENT = { plugin: "context-brake", key: "percent" };
5const ARMED = { plugin: "context-brake", key: "armed" };
6const FIRED = { plugin: "context-brake", key: "fired" };
7const STORE_KEY = "context-brake:threshold"; // persists across sessions
8
9const val = (r, fallback) => r?.value ?? fallback;
10
11async function threshold($) {
12 try {
13 const v = await $.store.get(STORE_KEY); // $.store.get resolves to the stored value itself
14 if (v === "off") return null;
15 return Number.isInteger(v) ? v : DEFAULT_THRESHOLD;
16 } catch {
17 return DEFAULT_THRESHOLD;
18 }
19}
20
21async function reading($) {
22 const { context } = await $.session.usage();
23 if (!context?.window) return null;
24 return context.percent ?? Math.round(((context.tokens ?? 0) / context.window) * 100);
25}
26
27async function update($) {
28 const percent = await reading($);
29 if (percent == null) return;
30 const limit = await threshold($);
31 const before = { armed: val(await $.state.get(ARMED), false), fired: val(await $.state.get(FIRED), false) };
32 const after = step(before, percent, limit);
33 await $.state.set(PERCENT, percent);
34 await $.state.set(ARMED, after.armed);
35 await $.state.set(FIRED, after.fired);
36 if (after.armed && !before.armed) {
37 try { $.ui.toast(`Context ${percent}% full: Claude will wrap up at a clean boundary. /compact when ready.`); } catch { /* no UI */ }
38 }
39}
40
41export function register(on) {
42 on("session.start", async ($, e, next) => {
43 const result = await next(e);
44 try {
45 await $.command.register({ name: "brake", description: "Context brake: /brake [status | <10-99> | off]" });
46 } catch { /* never block startup */ }
47 try { await update($); } catch { /* usage unavailable */ }
48 return result;
49 });
50
51 on("turn.complete", async ($, e, next) => {
52 const result = await next(e);
53 if (!e.agentId) {
54 try { await update($); } catch { /* usage unavailable */ }
55 }
56 return result;
57 });
58
59 // The only rewrite: attach the note once, as context beside the user's own prompt (never edits the prompt text).
60 on("prompt.submit", async ($, e, next) => {
61 let armed = false;
62 try { armed = val(await $.state.get(ARMED), false); } catch { /* fall through */ }
63 if (!armed) return next(e);
64 const percent = val(await $.state.get(PERCENT), 0);
65 await $.state.set(ARMED, false);
66 await $.state.set(FIRED, true);
67 return next({ ...e, context: [...(Array.isArray(e.context) ? e.context : []), note(percent)] });
68 });
69
70 on("command.run", { command: "brake" }, async ($, e) => {
71 const arg = String(e.args ?? "").trim();
72 if (arg && arg !== "status") {
73 const parsed = parseThreshold(arg);
74 if (!parsed) return { text: "Usage: /brake [status | <10-99> | off]" };
75 await $.store.set(STORE_KEY, parsed.off ? "off" : parsed.threshold);
76 await $.state.set(ARMED, false);
77 await $.state.set(FIRED, false);
78 try { await update($); } catch { /* ignore */ }
79 return { text: parsed.off ? "Context brake off." : `Context brake set to ${parsed.threshold}%.` };
80 }
81 const limit = await threshold($);
82 const percent = val(await $.state.get(PERCENT), null);
83 const fired = val(await $.state.get(FIRED), false);
84 const armed = val(await $.state.get(ARMED), false);
85 const where = percent == null ? "no reading yet" : `context ${percent}%`;
86 const status = limit == null ? "off" : armed ? "armed: the note goes out with your next prompt" : fired ? "already sent for this fill; re-arms after /compact or /clear" : "waiting";
87 return { text: `Context brake ${limit == null ? "" : `at ${limit}% `}(${where}): ${status}.` };
88 });
89}
90hooks/core.mjs 35 lines1// Pure decision logic for context-brake.
2
3export const DEFAULT_THRESHOLD = 80;
4export const REARM_GAP = 15; // percentage points below the threshold before the brake re-arms
5
6/**
7 * Next brake state after a context reading.
8 * - crosses threshold while not yet fired -> armed (note goes out with the next prompt)
9 * - drops REARM_GAP below threshold -> reset (after /compact or /clear)
10 */
11export function step(state, percent, threshold) {
12 const s = { armed: !!state?.armed, fired: !!state?.fired };
13 if (threshold == null || !Number.isFinite(percent)) return s;
14 if (percent >= threshold && !s.fired) return { armed: true, fired: false };
15 if (percent < threshold - REARM_GAP) return { armed: false, fired: false };
16 return s;
17}
18
19export function parseThreshold(arg) {
20 const a = String(arg ?? "").trim().toLowerCase();
21 if (a === "off") return { off: true };
22 const n = Number(a.replace(/%$/, ""));
23 if (a !== "" && Number.isInteger(n) && n >= 10 && n <= 99) return { threshold: n };
24 return null;
25}
26
27export function note(percent) {
28 return [
29 `[context-brake] The context window is ${percent}% full and auto-compaction is approaching.`,
30 "Finish the step you are on, then stop at a clean boundary instead of starting large new work.",
31 "Before stopping, give a short handoff: goal, what is done, what is left, key files and decisions.",
32 "This is a one-time note from a user-installed mod; the user may run /compact afterwards.",
33 ].join(" ");
34}
35types/index.d.ts 7 lines1declare module "claude-code" {
2 interface PluginState {
3 "context-brake": { percent: number | null; armed: boolean; fired: boolean }
4 }
5}
6export type ContextBrakeState = { percent: number | null; armed: boolean; fired: boolean }
7