Asks before sending a prompt into a big conversation whose prompt cache has gone cold

Function-hook mods for Claude Code: shell safety, secret guarding, session telemetry, context panels, and workflow tools. Each mod is an isolated plugin with its own tests, and you enable only the ones you want.
| Mod | Description |
|---|---|
guarda | Intercepts risky Bash commands (rm -rf, git reset --hard, force push, new dependencies) and asks for confirmation explaining the impact. |
lixeira | Sends rm / Remove-Item targets to the Windows Recycle Bin instead of deleting them permanently. |
cofre | Keeps API keys out of the conversation: /chave stores them in .env, secret file reads display only variable names, outputs are masked, and pasted keys are quarantined. |
atomic-commit-guard | Blocks commits that mix top-level directories, violate Conventional Commits, or lack a Co-Authored-By trailer. |
universal-audit-log | A hook on * that logs every event (tool calls, prompts, turns, fs/http/process calls from other plugins) with secret redaction. |
| Mod | Description |
|---|---|
context-monitor | Context weather and token usage above the prompt, replay and activity panels, plain-language explanation before each edit, and ai-memory notes. |
cache-frio | Confirms before submitting a prompt to a large conversation whose prompt cache has gone cold. |
session-telemetry | Timestamp on each response, TTFT and tok/s per step, and visual notice whenever the responding model switches. |
session-time-machine | /timemachine: renders the session as a timeline of prompts, tool calls, and turn completions. |
turn-inspector | Side pane displaying the session turn-by-turn: every model request and what it carried. |
clean-view | Collapses each successful tool call into a single concise line (Edit file +12 −3); errors and in-flight calls retain the full block. |
webfetch-cache | Serves repeated WebFetch requests (identical URL and prompt) directly from the session cache. |
reflexo | Detects corrections and constraints in prompts, offering to save them as rules in CLAUDE.md. |
| Mod | Description |
|---|---|
agent-flow | Live diagram of agent flow: main loop context, subagent tree, and context passed/returned by each agent. |
agent-radar | One live line per running subagent above the prompt; /radar opens a pane with messages from each. |
delegation-dock | Status line indicator showing in-flight delegations (agy/codex/ollama) and alerting when one returns empty. |
phase-tracker | Tracks and displays the current development phase (Spec → Red Test → Surgical Code → Verification). |
next-prompts | Band with 3 next-step suggestions generated by a free Ollama cloud model; click one to draft. |
git-sidebar | Lazygit-style side pane: worktrees, local branches, branch switching, and worktree checkout. |
wait-quiz | Quizzes a flashcard in a band above the prompt while Claude is working. |
meus-mods | Central hub: quick-access bar above the prompt and /mods command displaying the status of each mod. |
Requires Claude Code with function-hook mods support.
git clone https://github.com/Covre81/claude-code-mods.git ~/.claude/mods
Enable the mods by listing their absolute paths in CLAUDE_CODE_PLUGIN_DIRS inside ~/.claude/settings.json. On Windows, paths are separated by ;:
{
"env": {
"CLAUDE_CODE_PLUGIN_DIRS": "C:/Users/me/.claude/mods/guarda;C:/Users/me/.claude/mods/cofre;C:/Users/me/.claude/mods/session-telemetry"
}
}
Order matters: the last plugin in the list sits closest to the engine. For instance, lixeira should come after guarda so guarda inspects the original rm -rf first.
Several mods include a command to toggle them on or off without restarting (/webfetch-cache on|off, /radar-toggle, /telemetria…); /mods displays the status of all mods.
Each mod follows the same structure:
<mod>/
.claude-plugin/plugin.json manifest
.claude-plugin/types/ engine types (git-ignored, see below)
hooks/hooks.json hooks registered by the mod
hooks/register.ts(x) code
hooks/*.test.ts(x) tests
Engine types are not committed to the repository. To typecheck and test a mod, copy .claude-plugin/types from an existing mod or generate them with your Claude Code instance.
Tests run directly within Claude Code, not via vitest/jest:
claude plugin test guarda # single mod
for d in */hooks/hooks.json; do claude plugin test "${d%%/*}"; done # all mods
Commits follow Conventional Commits, scoped to one mod per commit (enforced by atomic-commit-guard).
Six mods (agent-flow, git-sidebar, session-time-machine, turn-inspector, universal-audit-log, webfetch-cache) originate from davila7/claude-code-templates, and agent-radar originates from hamzafer/claude-code-mods, both under the MIT License and adapted here. Copyright notices are preserved in THIRD_PARTY_NOTICES.md.
hooks/register.tsx 108 lines1import { atom, read, update } from 'claude-code'
2import type { Register, EngineInterface, Timer } from 'claude-code'
3
4const isEnabledRef = atom({ plugin: 'cache-frio', key: 'isEnabled' } as const, true)
5
6// ponytail: fixed 1h TTL — on overage the real TTL drops to 5m and this under-warns; no API exposes it here.
7// Upgrade: read the TTL from the event once the host exposes it (today only PreModelSwitch has cache_ttl).
8const TTL_MS = 60 * 60 * 1000
9const MIN_TOKENS = 100_000
10
11export function minutesLeft({ idleMs }: { idleMs: number }): number {
12 return Math.max(0, Math.floor((TTL_MS - idleMs) / 60_000))
13}
14
15/** Origins with a person at the keyboard: composer Enter and Remote Control. */
16const HUMAN = new Set(['composer', 'bridge'])
17const SEND = 'Mandar assim mesmo'
18const COMPACT = 'Compactar antes'
19const CANCEL = 'Cancelar'
20
21/** Cold = over 1h since the last main-loop answer, a context big enough to hurt, and a person sending. */
22export function isCold(p: { idleMs: number; tokens: number; origin: string }): boolean {
23 return p.idleMs > TTL_MS && p.tokens > MIN_TOKENS && HUMAN.has(p.origin)
24}
25
26const formatIdle = (ms: number) => {
27 const m = Math.floor(ms / 60_000)
28 return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, '0')}`
29}
30
31type State = { lastAnswerAt: number | undefined; ticker: Timer | undefined }
32
33/** Never rejects: the status line is decoration and must not break the hooks that call it. */
34async function refresh($: EngineInterface, state: State): Promise<void> {
35 try {
36 const isEnabled = await read($, isEnabledRef)
37 const idleMs = isEnabled && state.lastAnswerAt !== undefined ? (await $.clock.now()) - state.lastAnswerAt : undefined
38 const tokens = idleMs === undefined ? undefined : (await $.session.usage()).context.tokens
39 // No await below: check-and-assign on state.ticker stays atomic, so overlapping refreshes can't leak a timer.
40 if (idleMs === undefined || tokens === undefined || idleMs >= TTL_MS || tokens <= MIN_TOKENS) {
41 state.ticker?.cancel()
42 state.ticker = undefined
43 $.ui.status(undefined)
44 return
45 }
46 $.ui.status(`cache: ${minutesLeft({ idleMs })} min`)
47 state.ticker ??= $.clock.every(60_000, () => void refresh($, state))
48 } catch {
49 state.ticker?.cancel()
50 state.ticker = undefined
51 }
52}
53
54export const register: Register = on => {
55 const state: State = { lastAnswerAt: undefined, ticker: undefined }
56
57 on('session.start', async ($, e, next) => {
58 const saved = await $.store.get('isEnabled').catch(() => undefined)
59 await update($, isEnabledRef, () => (typeof saved === 'boolean' ? saved : isEnabledRef.initial))
60 await $.command.register({
61 name: 'cache-frio',
62 description: 'Liga/desliga o aviso de cache frio antes de mandar um prompt caro.',
63 argumentHint: 'on|off',
64 })
65 await refresh($, state)
66 return next(e)
67 })
68
69 on('command.run', { command: 'cache-frio' }, async ($, e) => {
70 const arg = e.args.trim().toLowerCase()
71 if (arg !== 'on' && arg !== 'off') {
72 const isEnabled = await read($, isEnabledRef)
73 return { text: `cache-frio esta ${isEnabled ? 'ativado' : 'desativado'}. Uso: /cache-frio on · /cache-frio off` }
74 }
75 await update($, isEnabledRef, () => arg === 'on')
76 await $.store.set('isEnabled', arg === 'on') // $.state is per session; the store survives a restart
77 await refresh($, state) // on shows the countdown now, off hides it
78 return { text: `cache-frio ${arg === 'on' ? 'ativado' : 'desativado'}.` }
79 })
80
81 on('turn.complete', async ($, e, next) => {
82 if (!e.agentId) state.lastAnswerAt = await $.clock.now()
83 await refresh($, state)
84 return next(e)
85 }).catch(($, e, next) => next(e))
86
87 on('prompt.submit', async ($, e, next) => {
88 if (!(await read($, isEnabledRef))) return next(e)
89 const idleMs = state.lastAnswerAt === undefined ? undefined : (await $.clock.now()) - state.lastAnswerAt
90 const tokens = (await $.session.usage()).context.tokens // free without `breakdown`
91 if (idleMs === undefined || tokens === undefined || !isCold({ idleMs, tokens, origin: e.origin.kind })) return next(e)
92 const choice = await $.ui.ask(
93 `O cache esfriou (${formatIdle(idleMs)} parado). Reler ${Math.round(tokens / 1000)} mil tokens custa caro. O que fazer?`,
94 { header: 'Cache frio', options: [SEND, COMPACT, CANCEL] },
95 )
96 if (choice !== COMPACT && choice !== CANCEL) return next(e) // SEND, or free text under "Other"
97 // The host refuses $.session.compact() inside prompt.submit (it would compact under the held turn),
98 // so hold the prompt, put it back in the box, and let the user run the command.
99 const isFilled = (await $.prompt.fill({ text: e.text }).catch(() => undefined))?.isFilled === true
100 const step =
101 choice === COMPACT
102 ? 'rode /compact e depois mande de novo (compactar também relê o contexto inteiro uma vez)'
103 : 'rode /clear e depois mande de novo: é o que de fato economiza'
104 const where = isFilled ? 'O texto voltou para a caixa' : 'Copie o texto abaixo'
105 return { drop: `Não enviado: cache frio. ${where}; ${step}.\n\n${e.text}` }
106 }).catch(($, e, next) => next(e)) // fail open: a bug or a dismissed / unanswerable ask (-p) must never eat the prompt
107}
108types/index.d.ts 12 lines1export type CacheFrioState = {
2 isEnabled: boolean
3}
4
5declare module 'claude-code' {
6 interface PluginState {
7 'cache-frio': {
8 isEnabled: boolean
9 }
10 }
11}
12