Context-window gauge with a pre-compaction warning, and a log of what was in flight at each compaction

Five Claude Code mods built from how I actually use Claude Code: four to six sessions at a time, mostly in one repo, a lot of PRs going to production, and a pile of rules I keep forgetting to check.
Claude built all five. The full story, with screenshots, videos and the bugs a real run caught, is on my site: I asked Claude to read 30 of my Claude Code sessions and build the mods I needed

This is what I gave Claude Code. It read my last 30 sessions, came back with ten ideas, and built all ten as these five mods.
look at the last 30 sessions and see how we use claude to work - then look at https://code.claude.com/docs/en/plugins/mods/overview and suggest 10 possible mods for our workflow that would help me be better informed as to what each session is doing.
Run it on your own sessions. Your ten ideas will probably come out different from mine, and I'd like to see them.
| Mod | What it gives you | Commands |
|---|---|---|
| session-fleet | A board of every live session, a "needs you" row when another session is blocked on you, and a guard when two sessions write to the same worktree | /fleet |
| ship-tracker | A row per PR: CI → merged → production deployment → live, a warning when a merge never deploys, and a receipt line after every answer | /ship-track <pr> |
| session-hygiene | Configurable tripwires (typecheck before push, review before push, mutation-check new tests and more), plus a handoff card of loose ends from earlier sessions | /handoff, /handoff clear |
| session-activity | A ledger of everything that left the machine, a hold on database writes, and what each session is waiting on | /ledger, /waiting |
| context-gauge | Context fill in the status line, warnings before compaction, and a snapshot of in-flight work that survives compaction | /context-log |
They work on their own. Install any one, or all five.
You need a Claude Code version with mods (see the mods docs), git, and for ship-tracker and the handoff card, the GitHub CLI signed in.
A mod is code that runs with your permissions inside Claude Code. Read it before you install it. Each one is a single hooks/register.tsx file, and claude plugin validate <folder> lists exactly which events it hooks and what it calls.
In a Claude Code session:
/plugin marketplace add bennewton999/claude-code-mods
/plugin install session-fleet@bennewton-mods
/plugin install ship-tracker@bennewton-mods
/plugin install session-hygiene@bennewton-mods
/plugin install session-activity@bennewton-mods
/plugin install context-gauge@bennewton-mods
Then /reload-plugins, or start a new session.
git clone https://github.com/bennewton999/claude-code-mods ~/claude-code-mods
Load them in every session by adding the folders to env in ~/.claude/settings.json (colon-separated):
{
"env": {
"CLAUDE_CODE_PLUGIN_DIRS": "/Users/you/claude-code-mods/session-fleet:/Users/you/claude-code-mods/ship-tracker:/Users/you/claude-code-mods/session-hygiene:/Users/you/claude-code-mods/session-activity:/Users/you/claude-code-mods/context-gauge",
"CLAUDE_CODE_PLUGIN_DIR_WATCH": "1"
}
}
Or try one for a single session:
claude --plugin-dir ~/claude-code-mods/session-fleet
CLAUDE_CODE_PLUGIN_DIR_WATCH makes desktop app sessions hot-reload a mod when its files change (an interactive terminal session already does). A session reads it at startup, so sessions that were already open pick up the mods, and the watch, after one restart.
Every session writes one record to a key-value store that all sessions on the machine share ($.store), with a heartbeat every 30 seconds. Three minutes without a heartbeat and the session counts as gone.
/fleet opens a pane with every live session: a status dot, its first prompt (there's no session title API), the worktrees it's touching, branch, PR, last action and how long ago. Waiting sessions sort to the top, and a worktree two live sessions share gets a ⚠.projectPills to Label:regex:color entries separated by ;, the regex tested against the repo folder name: "pluginConfigs": {
"session-fleet": {
"options": { "projectPills": "Work:acme|billing:#2563eb; Blog:blog:#059669" }
}
}

gh pr output, or add one with /ship-track 1107 or /ship-track owner/repo#1107.no prod deploy!. That's the silent Vercel git-trigger failure that got me before.
Tripwires for rules you keep forgetting. When one trips you get a transcript line and a toast, and Claude gets a reminder with the tool result so it can act on it.
Every rule is a plugin option. You set them on the install screen, or later in /config. Out of the box only the two generic rules are on; the rest stay off until you point them at your repos.
| Option | Default | What it does |
|---|---|---|
holdEnvSourcing | on | Asks Run it or Stop before a command sources a .env file into the shell |
mutationCheckTests | on | When Claude writes a new test file, reminds it to prove the test fails without the code it covers |
reviewBeforePush | off | Flags a git push with edits made since the last local /code-review |
uncheckedRepos | none | Path fragments (/my-app/) of repos whose CI doesn't typecheck or build. There it flags a push after TypeScript edits with no typecheck, and a merge after package.json or next.config changes with no local build |
middlewareRouteRepos | none | Next.js repos whose src/middleware.ts allowlists routes. A new top-level src/app/<route>/page.tsx gets a reminder to add it |
bannedStrings | none | Case-insensitive text that should never be written into a file |
supabase | off | Asks before a command uses a service-role key; after apply_migration, reminds Claude to run NOTIFY pgrst, 'reload schema' and to revoke default grants on new tables |
If you load the mod from a clone instead of the marketplace, set the same options in ~/.claude/settings.json:
{
"pluginConfigs": {
"session-hygiene": {
"options": {
"uncheckedRepos": ["/my-app/"],
"bannedStrings": ["lorem ipsum"],
"supabase": true
}
}
}
}
The handoff card: after each turn a session saves its open loops (open PRs, uncommitted or unpushed worktrees, a question it left you). A new session lists loops from sessions that ended or went quiet, re-checked live first. /handoff shows them again, /handoff clear dismisses them.

/ledger lists everything that left the machine: MCP write tools, git push, gh pr create and merge, gh api writes, Vercel deploys, curl writes, publishes, SQL writes. A button switches to all sessions in the last 24 hours. Reads stay out of it.execute_sql that writes (INSERT, UPDATE, DELETE, DDL, GRANT…) stops for a Run it or Stop question. SELECTs and NOTIFY pass. Words inside comments and quoted strings don't count.Thinking · 2 bg · agent 3m…), and a row above the prompt lists background commands, agents, workflows and monitors still running after Claude stops. /waiting lists them, /waiting clear resets.
ctx 62% · 124k/200k · compacted 1×./context-log lists every compaction this session with its snapshot.
Everything these mods record stays on your machine in Claude Code's per-plugin store. The only network calls are gh requests to GitHub for PR and deployment state (ship-tracker, and the handoff card's re-check).
/ledger reported the pane as opened but it never drew. It works in fresh sessions; the built-in diff pane seemed to be in front of it.I'm Ben Newton. I write about building with AI at benenewton.com, and I'm building BlackOps Center, the platform that runs my site, my posts and the notes these Claude sessions write into. If you want to see it, start at blackopscenter.com/start.
Claude Code wrote the mods with its built-in plugin-authoring skill. Issues and PRs are welcome.
MIT licensed.
hooks/register.tsx 243 lines1import type { EngineInterface as Engine, Register } from 'claude-code'
2
3// context-gauge:
4// 9. a status-line gauge of the context window (percent, tokens, compactions
5// so far), toasts as it crosses 70% and 85%, and a band row from 80% with
6// a "Compact now" button. At each compaction it tells the summarizer what
7// was in flight, logs that snapshot, and hands it back to Claude with the
8// next prompt. /context-log lists the compactions of this session.
9
10const LOG = 'c:'
11const WARN_AT = 80
12const TOAST_AT = [70, 85]
13
14type Compaction = {
15 ts: number
16 trigger: string
17 percent: number | null
18 tokensBefore: number | null
19 tokensAfter: number | null
20 snapshot: string
21}
22
23let sessionId = ''
24let percent: number | null = null
25let tokens: number | null = null
26let windowSize: number | null = null
27let toasted = new Set<number>()
28let compactions: Compaction[] = []
29let reinject: string | null = null
30
31// What the session is in the middle of, gathered from its own events
32let task = ''
33let lastPrompt = ''
34const edited: string[] = []
35const actions: string[] = []
36const links = new Set<string>()
37
38const HUMAN = new Set(['composer', 'bridge', 'sdk'])
39
40const short = (s: string, n: number) => {
41 const one = s.replace(/\s+/g, ' ').trim()
42 return one.length > n ? one.slice(0, n - 1) + '…' : one
43}
44const k = (n: number) => (n >= 1000 ? `${Math.round(n / 1000)}k` : String(n))
45const hhmm = (ts: number) => new Date(ts).toTimeString().slice(0, 5)
46
47function snapshot(): string {
48 const lines = [`Task: ${task || '(unknown)'}`]
49 if (lastPrompt && lastPrompt !== task) lines.push(`Latest request: ${lastPrompt}`)
50 if (edited.length) lines.push(`Files edited: ${edited.slice(-12).join(', ')}`)
51 if (links.size) lines.push(`PRs/links in play: ${[...links].slice(-5).join(', ')}`)
52 if (actions.length) lines.push(`Last actions: ${actions.slice(-8).join(' | ')}`)
53 return lines.join('\n')
54}
55
56async function measure($: Engine) {
57 try {
58 const u = await $.session.usage()
59 percent = typeof u.context.percent === 'number' ? Math.round(u.context.percent) : null
60 tokens = u.context.tokens ?? null
61 windowSize = u.context.window
62 } catch {
63 return
64 }
65 const bits: string[] = []
66 if (percent !== null) bits.push(`ctx ${percent}%`)
67 if (tokens !== null && windowSize) bits.push(`${k(tokens)}/${k(windowSize)}`)
68 if (compactions.length) bits.push(`compacted ${compactions.length}×`)
69 $.ui.status(bits.length ? bits.join(' · ') : undefined)
70
71 if (percent !== null) {
72 for (const t of TOAST_AT) {
73 if (percent >= t && !toasted.has(t)) {
74 toasted.add(t)
75 $.ui.toast(
76 t >= 85
77 ? `Context at ${percent}%: compaction is close. /compact now keeps the in-flight work in the summary.`
78 : `Context at ${percent}%`,
79 { timeoutMs: t >= 85 ? 12_000 : 6000 },
80 )
81 }
82 }
83 }
84 $.ui.invalidate('ui.render')
85}
86
87async function persist($: Engine) {
88 if (sessionId) await $.store.set(LOG + sessionId, compactions)
89}
90
91export const register: Register = on => {
92 on('session.start', async ($, e, next) => {
93 sessionId = await $.session.id()
94 compactions = ((await $.store.get(LOG + sessionId)) as Compaction[] | undefined) ?? []
95 // Drop logs of sessions untouched for a week
96 const now = await $.clock.now()
97 for (const key of await $.store.keys()) {
98 if (!key.startsWith(LOG) || key === LOG + sessionId) continue
99 const list = (await $.store.get(key)) as Compaction[] | undefined
100 const last = list?.[list.length - 1]?.ts ?? 0
101 if (now - last > 7 * 24 * 60 * 60_000) await $.store.delete(key)
102 }
103 // A resumed session starts a new process: recover the task from the transcript
104 if (!task) {
105 try {
106 const msgs = await $.session.messages()
107 const human = msgs.filter(m => m.role === 'user' && m.text && !m.text.startsWith('<'))
108 if (human[0]) task = short(human[0].text, 120)
109 const latest = human[human.length - 1]
110 if (latest) lastPrompt = short(latest.text, 160)
111 } catch {
112 // no transcript yet
113 }
114 }
115 await $.command.register({
116 name: 'context-log',
117 description: 'Context fill now, and what was in flight at each compaction this session',
118 })
119 void measure($)
120 return next(e)
121 })
122
123 on('command.run', { command: 'context-log' }, async $ => {
124 await measure($)
125 const head = `Context: ${percent ?? '?'}%${tokens !== null && windowSize ? ` (${k(tokens)} of ${k(windowSize)})` : ''} · ${compactions.length} compaction${compactions.length === 1 ? '' : 's'} this session`
126 const body = compactions.map(
127 (c, i) =>
128 `#${i + 1} ${hhmm(c.ts)} ${c.trigger}${c.percent !== null ? ` at ${c.percent}%` : ''}${c.tokensBefore && c.tokensAfter ? ` (${k(c.tokensBefore)} → ${k(c.tokensAfter)})` : ''}\n${c.snapshot.replace(/^/gm, ' ')}`,
129 )
130 return { text: [head, ...body].join('\n\n') }
131 })
132
133 on('session.measure', async ($, e, next) => {
134 if (e.changed.includes('context')) void measure($)
135 return next(e)
136 })
137
138 on('prompt.submit', async ($, e, next) => {
139 const kind = e.origin?.kind
140 if ((!kind || HUMAN.has(kind)) && !e.text.startsWith('<')) {
141 if (!task) task = short(e.text, 120)
142 lastPrompt = short(e.text, 160)
143 }
144 if (reinject) {
145 const text = reinject
146 reinject = null
147 return next({
148 ...e,
149 context: [...(e.context ?? []), `[context-gauge] Just before the last compaction, this session was:\n${text}`],
150 })
151 }
152 return next(e)
153 })
154
155 on('tool.call', async ($, e, next) => {
156 const ran = await next(e)
157 if (e.agentId !== undefined) return ran
158 const call = e as unknown as { tool: string } & Record<string, unknown>
159 const fp = (call.file_path ?? call.notebook_path) as string | undefined
160 if (['Edit', 'Write', 'NotebookEdit', 'MultiEdit'].includes(call.tool) && typeof fp === 'string') {
161 const name = fp.replace(/^\/Users\/[^/]+/, '~')
162 const i = edited.indexOf(name)
163 if (i >= 0) edited.splice(i, 1)
164 edited.push(name)
165 if (edited.length > 30) edited.shift()
166 }
167 const what =
168 call.tool === 'Bash' && typeof call.command === 'string'
169 ? `$ ${short(call.command, 60)}`
170 : typeof fp === 'string'
171 ? `${call.tool} ${fp.split('/').pop()}`
172 : call.tool.startsWith('mcp__')
173 ? (call.tool.split('__').pop() ?? call.tool)
174 : call.tool
175 actions.push(`${what}${ran.isError ? ' (failed)' : ''}`)
176 if (actions.length > 20) actions.shift()
177 for (const m of (ran.text ?? '').matchAll(/https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/\d+/g)) links.add(m[0])
178 return ran
179 })
180
181 on('turn.complete', async ($, e, next) => {
182 const r = await next(e)
183 if (e.agentId === undefined) void measure($)
184 return r
185 })
186
187 on('session.compact', async ($, e, next) => {
188 if (e.agentId !== undefined || e.trigger === 'precompute') return next(e)
189 const snap = snapshot()
190 const before = percent
191 // Ask the summarizer to keep the in-flight state
192 const keep = `Preserve exactly, as its own section: the in-flight work below, plus any open PR numbers, branch and worktree names, pending questions to the user, and decisions not yet acted on.\n${snap}`
193 const result = await next({ ...e, instructions: e.instructions ? `${e.instructions}\n\n${keep}` : keep })
194 if ('skip' in result && result.skip !== undefined) return result
195
196 const entry: Compaction = {
197 ts: await $.clock.now(),
198 trigger: e.trigger,
199 percent: before,
200 tokensBefore: result.tokensBefore ?? null,
201 tokensAfter: result.tokensAfter ?? null,
202 snapshot: snap,
203 }
204 compactions.push(entry)
205 compactions = compactions.slice(-20)
206 await persist($)
207 reinject = snap
208 toasted = new Set()
209 $.ui.log(
210 `Compaction #${compactions.length} (${e.trigger}${before !== null ? ` at ${before}%` : ''}). In flight: ${short(task || lastPrompt || 'unknown', 80)}${edited.length ? ` · ${edited.length} files edited` : ''}. /context-log for the full snapshot.`,
211 )
212 void measure($)
213 return result
214 })
215
216 // Band row once the window is nearly full
217 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
218 const below = await next(e)
219 if (e.props.hasSurvey || percent === null || percent < WARN_AT) return below
220 const { Box, Text, Button } = $.ui.resolve(e)
221 return (
222 <Box flexDirection="column">
223 <Box gap={1}>
224 <Text color={percent >= 90 ? 'red' : 'yellow'}>
225 ◔ Context {percent}%{tokens !== null && windowSize ? ` (${k(tokens)}/${k(windowSize)})` : ''}: compaction is near
226 </Text>
227 <Button
228 key="compact-now"
229 label="Compact now"
230 plain
231 onPress={async () => {
232 await $.session.compact({
233 instructions: `Preserve exactly, as its own section, the in-flight work:\n${snapshot()}`,
234 })
235 }}
236 />
237 </Box>
238 {below}
239 </Box>
240 )
241 })
242}
243