SLOPSHOPPER

context-brake

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…

newcommandtoastprompt
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · context-brake
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /brake ⎿ context-brake: Context brake at 80% (context 49%): waiting. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Claude Code Mods: context receipt & context 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.

ModWhat it doesReach
context-receiptAfter 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-brakeWhen 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.

context-receipt rendered from the real runtime

The GIF is drawn by scripts/make-demo.py from 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.

The problem

Context fill drives real decisions mid-session (keep going, delegate, compact, hand off), and the ambient indicator for it keeps disappearing:

  • anthropics/claude-code#90708 – Desktop app: bring back the context-usage indicator. Users describe sessions hitting auto-compact or "Prompt is too long" with no lead-up.
  • Related reports linked from that thread: #91232, #91385 (compaction with no warning), #102094, #102151 (context missing from the usage ring/popover).
  • anthropics/claude-code#91870 – the Claude Mods proposal, where several users ask for a way to have Claude stop "at a clean boundary and let it compact, rather than have compaction land in the middle of a verification run".

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:

  1. Attribution per tool result, per turn. A meter says "61%". The receipt says the 40k jump was one Read of a log file, so you know what to avoid next time.
  2. Telling Claude, not only you. A toast helps if you are watching. The brake puts the information where it changes behaviour: in front of the model, once, before compaction lands mid-task.

Both work in the terminal and the desktop app (the band uses the AbovePrompt component; the brake has no UI beyond a toast).

Install

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

Usage

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.

How it is built

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.

Testing

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.

Privacy

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.

License

MIT © 2026 biqpro SH. Not affiliated with Anthropic.

Source 3 files
hooks/context-brake.mjs 90 lines
1// 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}
90
hooks/core.mjs 35 lines
1// 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}
35
types/index.d.ts 7 lines
1declare 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