Central mods hub: quick-access bar above the prompt and /mods command displaying mod status.

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 296 lines1import type { EngineInterface, Register } from 'claude-code'
2
3const PANEL_ID = 'meus-mods-panel'
4const BRAND = '#D97757'
5const BG = '#11141c'
6const BG_ALT = '#171b26'
7const TOGGLE_ON = '#2ecc71'
8const TOGGLE_OFF = '#ff5252'
9const TOGGLE_KNOB = '#f4f4f4'
10// Blocos de cor de fundo, sem glifo nenhum: funciona em qualquer fonte, de
11// qualquer superfície (terminal, desktop, vscode, mobile). A versão anterior
12// usava glifos de meio-círculo para a bolinha, mas isso depende da fonte do
13// host desenhar os dois meios encostados — em algumas fontes vira quadradinho,
14// em outras vira dois meios soltos sem preenchimento.
15const TOGGLE_FILL = ' '
16const TOGGLE_KNOB_BLOCK = ' '
17
18const memoriaRef = { plugin: 'context-monitor', key: 'memoriaEnabled' } as const
19// O engine só aceita ref de estado escrita literalmente (para listar o que cada
20// mod lê), então o on/off de cada mod precisa de uma linha aqui. Mod sem ref
21// ainda aparece e conta; só fica sem toggle até ganhar a sua.
22const cleanViewRef = { plugin: 'clean-view', key: 'isEnabled' } as const
23const cacheFrioRef = { plugin: 'cache-frio', key: 'isEnabled' } as const
24const delegationDockRef = { plugin: 'delegation-dock', key: 'isEnabled' } as const
25const contextMonitorRef = { plugin: 'context-monitor', key: 'isEnabled' } as const
26const waitQuizRef = { plugin: 'wait-quiz', key: 'isEnabled' } as const
27const nextPromptsRef = { plugin: 'next-prompts', key: 'isEnabled' } as const
28const webfetchCacheRef = { plugin: 'webfetch-cache', key: 'isEnabled' } as const
29const universalAuditLogRef = { plugin: 'universal-audit-log', key: 'isEnabled' } as const
30const gitSidebarRef = { plugin: 'git-sidebar', key: 'isEnabled' } as const
31const turnInspectorRef = { plugin: 'turn-inspector', key: 'isEnabled' } as const
32const agentFlowRef = { plugin: 'agent-flow', key: 'isEnabled' } as const
33const sessionTimeMachineRef = { plugin: 'session-time-machine', key: 'isEnabled' } as const
34const phaseTrackerRef = { plugin: 'phase-tracker', key: 'isEnabled' } as const
35const sessionTelemetryRef = { plugin: 'session-telemetry', key: 'isEnabled' } as const
36const agentRadarRef = { plugin: 'agent-radar', key: 'isEnabled' } as const
37const atomicCommitGuardRef = { plugin: 'atomic-commit-guard', key: 'isEnabled' } as const
38
39async function enabledStates($: EngineInterface): Promise<Record<string, boolean | undefined>> {
40 const [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p] = await Promise.all([
41 $.state.get(cleanViewRef),
42 $.state.get(cacheFrioRef),
43 $.state.get(delegationDockRef),
44 $.state.get(contextMonitorRef),
45 $.state.get(waitQuizRef),
46 $.state.get(nextPromptsRef),
47 $.state.get(webfetchCacheRef),
48 $.state.get(universalAuditLogRef),
49 $.state.get(gitSidebarRef),
50 $.state.get(turnInspectorRef),
51 $.state.get(agentFlowRef),
52 $.state.get(sessionTimeMachineRef),
53 $.state.get(phaseTrackerRef),
54 $.state.get(sessionTelemetryRef),
55 $.state.get(agentRadarRef),
56 $.state.get(atomicCommitGuardRef),
57 ])
58 return {
59 // Nenhum default copiado aqui: todo mod publica o próprio estado no
60 // session.start; sem publicação (o mod não carregou), rows mostra desligado.
61 'clean-view': a.value,
62 'cache-frio': b.value,
63 'delegation-dock': c.value,
64 'context-monitor': d.value,
65 'wait-quiz': e.value,
66 'next-prompts': f.value,
67 'webfetch-cache': g.value,
68 'universal-audit-log': h.value,
69 'git-sidebar': i.value,
70 'turn-inspector': j.value,
71 'agent-flow': k.value,
72 'session-time-machine': l.value,
73 'phase-tracker': m.value,
74 'session-telemetry': n.value,
75 'agent-radar': o.value,
76 'atomic-commit-guard': p.value,
77 }
78}
79
80type Row = {
81 command: string | null
82 icon: string
83 accent: string
84 name: string
85 description: string
86 isEnabled: boolean | null // null = sempre ativo, sem toggle
87 isFeature?: boolean // recurso dentro de um mod: aparece, mas não conta como mod
88}
89
90type Meta = { icon: string; accent: string; description: string; command?: string | null }
91
92// Só o visual de cada mod conhecido. Quem está instalado vem do disco (rows);
93// mod novo sem entrada aqui aparece com o visual genérico e a descrição do
94// próprio plugin.json. `command` só existe quando o toggle não tem o nome do
95// mod; `null` diz que o mod não tem toggle.
96const META: Record<string, Meta> = {
97 'clean-view': { icon: '🧹', accent: '#5fd98a', description: 'Resume cada chamada de ferramenta numa linha' },
98 'cache-frio': { icon: '🧊', accent: '#5fb8d9', description: 'Avisa antes de mandar um prompt caro com cache frio' },
99 'delegation-dock': { icon: '🛰', accent: '#c97fe0', description: 'Status das delegações em voo (agy/codex/ollama)' },
100 'context-monitor': { icon: '🌤', accent: '#4aa3df', description: 'Clima e caixa de contexto, uso 5h/7d, replay e atividade', command: 'monitor' },
101 'wait-quiz': { icon: '🎴', accent: '#e8955c', description: 'Quiz de estudo na faixa enquanto Claude trabalha' },
102 'next-prompts': { icon: '💡', accent: '#f0d75c', description: 'Sugestões de próximo passo acima do prompt' },
103 'webfetch-cache': { icon: '🌐', accent: '#4fd1c5', description: 'Cache de WebFetch repetido na mesma sessão' },
104 'universal-audit-log': { icon: '📜', accent: '#a0a4ab', description: 'Log JSONL de todo evento do engine' },
105 'git-sidebar': { icon: '🌿', accent: '#7ee787', description: 'Worktrees e branches num painel lateral' },
106 'turn-inspector': { icon: '🪟', accent: '#d9875f', description: 'Cada turno do modelo com a janela de contexto', command: 'turns' },
107 'agent-flow': { icon: '🌳', accent: '#7aa6da', description: 'Árvore ao vivo dos subagentes e o contexto entre eles' },
108 'session-time-machine': { icon: '⏱', accent: '#58a6ff', description: 'Linha do tempo da sessão; bifurca a partir de qualquer ponto', command: 'timemachine' },
109 'phase-tracker': { icon: '🎯', accent: '#4aa3df', description: 'Fases da tarefa guiadas pelo método Karpathy', command: 'fase' },
110 'session-telemetry': { icon: '🕒', accent: '#f08a5d', description: 'Hora nas respostas e aviso de troca de modelo', command: 'telemetria' },
111 // radar-toggle ignora o argumento e inverte o estado; o painel sempre manda o oposto do atual, então dá no mesmo
112 'agent-radar': { icon: '📡', accent: '#e06c75', description: 'Faixa com o que cada subagente está fazendo', command: 'radar-toggle' },
113 'atomic-commit-guard': { icon: '⚛', accent: '#d19a66', description: 'Barra commit que mistura escopos ou tem mensagem ruim', command: 'commit-guard' },
114 'meus-mods': { icon: '✦', accent: BRAND, description: 'Este painel', command: null },
115 guarda: { icon: '🛡', accent: '#e0c84a', description: 'Segura comando destrutivo e pede confirmação', command: null },
116 cofre: { icon: '🔐', accent: '#5fd98a', description: 'Mantém chave de API fora da conversa', command: null },
117 lixeira: { icon: '🗑', accent: '#8a8f99', description: 'Manda rm para a Lixeira do Windows', command: null },
118}
119
120const GENERIC = { icon: '🧩', accent: '#a0a4ab' }
121
122// Separador de lista de caminhos: ';' no Windows, ':' no resto — mas sem
123// cortar o ':' de "C:/" ou "C:\\".
124const splitDirs = (raw: string) =>
125 raw
126 .split(/;|:(?![\\/])/)
127 .map(d => d.trim().replace(/[\\/]+$/, ''))
128 .filter(Boolean)
129
130async function manifest($: EngineInterface, dir: string): Promise<{ name: string; description: string }> {
131 const fallback = { name: dir.split(/[\\/]/).pop() ?? dir, description: '' }
132 try {
133 const json = JSON.parse(await $.fs.read(`${dir}/.claude-plugin/plugin.json`)) as { name?: unknown; description?: unknown }
134 return {
135 name: typeof json.name === 'string' ? json.name : fallback.name,
136 description: typeof json.description === 'string' ? json.description : '',
137 }
138 } catch {
139 return fallback
140 }
141}
142
143async function rows($: EngineInterface): Promise<Row[]> {
144 const dirs = splitDirs((await $.env.get('CLAUDE_CODE_PLUGIN_DIRS')) ?? '')
145 const states = await enabledStates($)
146
147 const mods = await Promise.all(
148 dirs.map(async (dir): Promise<Row> => {
149 const { name, description } = await manifest($, dir)
150 const meta = META[name]
151 // Sem ref de estado não há como mostrar on/off, então nem toggle.
152 // Convenção dos mods daqui: o toggle tem o nome do próprio mod.
153 const hasToggle = name in states && meta?.command !== null
154 const command = hasToggle ? (meta?.command ?? name) : null
155 const isEnabled = hasToggle ? (states[name] ?? false) : null
156 return { command, name, isEnabled, icon: meta?.icon ?? GENERIC.icon, accent: meta?.accent ?? GENERIC.accent, description: meta?.description ?? description }
157 }),
158 )
159
160 const extras: Row[] = []
161 if (mods.some(m => m.name === 'context-monitor')) {
162 const { value: memoria = false } = await $.state.get(memoriaRef)
163 extras.push({ command: 'memoria', icon: '🧠', accent: '#9b8cf0', name: 'memoria', description: 'Busca notas do ai-memory antes de cada prompt (recurso do context-monitor)', isEnabled: memoria, isFeature: true })
164 }
165
166 return [...mods, ...extras].sort((a, b) => a.name.localeCompare(b.name))
167}
168
169export const register: Register = on => {
170 on('session.start', async ($, e, next) => {
171 await $.command.register({
172 name: 'mods',
173 description: 'Abre o painel Meus Mods: estado e atalhos dos mods instalados.',
174 })
175 return next(e)
176 })
177
178 on('command.run', { command: 'mods' }, async $ => {
179 await $.ui.open({ id: PANEL_ID, title: 'Meus Mods' })
180 return { text: 'Painel Meus Mods aberto.' }
181 })
182
183 // context-monitor e next-prompts já disputam o AbovePrompt entre si e
184 // resolvem empilhando: cada um pede next(e) primeiro e só então desenha a
185 // própria linha por cima do que estiver embaixo. Este hook segue o mesmo
186 // padrão para não arrancar a faixa de ninguém.
187 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
188 if (e.props.hasSurvey) return next(e)
189
190 const { Box, Text, Button } = $.ui.resolve(e)
191 const below = await next(e)
192
193 const mine = (
194 <Box width={e.props.bodyColumns} flexDirection="row" justifyContent="flex-end">
195 <Button key="open-panel" plain hotkey="m" onPress={() => $.ui.open({ id: PANEL_ID, title: 'Meus Mods' })}>
196 <Text color={BRAND}>✦ mods</Text>
197 </Button>
198 </Box>
199 )
200
201 return below.type === 'engine' ? mine : (
202 <Box flexDirection="column">
203 {below}
204 {mine}
205 </Box>
206 )
207 })
208
209 on('ui.render', { component: 'Pane', requestId: PANEL_ID }, async ($, e) => {
210 const list = await rows($)
211 const mods = list.filter(r => !r.isFeature)
212 const active = mods.filter(r => r.isEnabled !== false).length
213 const { Box, Text, Button } = $.ui.resolve(e)
214
215 const toggle = (row: Row) => {
216 if (!row.command) return
217 void $.command.run({ command: row.command, args: row.isEnabled ? 'off' : 'on' })
218 }
219
220 // Pílula: faixa de cor de fundo + bolinha, sem glifo nenhum. Dois Text
221 // fixos (sem array, sem loop) — o motor de UI dos paineis não é React de
222 // verdade e não aceita lista de elementos como filho; array quebrava o
223 // render e o toggle inteiro desaparecia.
224 const Toggle = (row: Row) => {
225 const track = row.isEnabled ? TOGGLE_ON : TOGGLE_OFF
226 return (
227 <Button key={`${row.name}-toggle`} plain onPress={() => toggle(row)}>
228 {row.isEnabled ? (
229 <Text>
230 <Text backgroundColor={track}>{TOGGLE_FILL}</Text>
231 <Text backgroundColor={TOGGLE_KNOB}>{TOGGLE_KNOB_BLOCK}</Text>
232 </Text>
233 ) : (
234 <Text>
235 <Text backgroundColor={TOGGLE_KNOB}>{TOGGLE_KNOB_BLOCK}</Text>
236 <Text backgroundColor={track}>{TOGGLE_FILL}</Text>
237 </Text>
238 )}
239 </Button>
240 )
241 }
242
243 const renderRow = (row: Row, i: number) => (
244 <Box key={row.name} flexDirection="row" gap={1} paddingX={1} backgroundColor={i % 2 ? BG_ALT : undefined}>
245 <Text color={row.accent} bold>
246 ▍
247 </Text>
248 <Text>{row.icon}</Text>
249 <Box flexDirection="column" flexGrow={1}>
250 <Text bold color={row.accent}>
251 {row.name}
252 </Text>
253 <Text dimColor wrap="truncate-end">
254 {row.description}
255 </Text>
256 </Box>
257 {row.isEnabled === null ? <Text dimColor>sempre ativo</Text> : Toggle(row)}
258 </Box>
259 )
260
261 const toggleable = list.filter(r => r.isEnabled !== null)
262 const alwaysOn = list.filter(r => r.isEnabled === null)
263
264 return (
265 <Box flexDirection="column" borderStyle="round" borderColor={BRAND} backgroundColor={BG} paddingY={0} gap={1}>
266 <Box flexDirection="row" backgroundColor={BRAND} paddingX={1}>
267 <Text bold color="#1a0f08">
268 ✦ MEUS MODS
269 </Text>
270 </Box>
271
272 <Box flexDirection="column" paddingX={1}>
273 <Text dimColor>
274 {active} de {mods.length} mods ativos
275 </Text>
276 </Box>
277
278 <Box flexDirection="column">{toggleable.map((row, i) => renderRow(row, i))}</Box>
279
280 <Box flexDirection="column" paddingX={1}>
281 <Text bold dimColor>
282 sempre ativos
283 </Text>
284 </Box>
285 <Box flexDirection="column">{alwaysOn.map((row, i) => renderRow(row, i))}</Box>
286
287 <Box paddingX={1}>
288 <Button key="panel-close" role="dismiss" onPress={() => $.ui.close({ id: PANEL_ID })}>
289 Fechar
290 </Button>
291 </Box>
292 </Box>
293 )
294 })
295}
296