SLOPSHOPPER

meus-mods

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

newpanebandcommand
v0.1.0MITupdated 2026-10-19Covre81/claude-code-mods/meus-mods
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · meus-mods
│ ┃ Meus Mods ✕ › fix the failing auth test and add an audit log call │ ┃ ╭──────────────────────────────────────────╮ │ ┃ │ ✦ MEUS MODS │ ⏺ Read(src/auth.ts) │ ┃ │ │ ⎿ Read 6 lines │ ┃ │ 0 de 0 mods ativos │ ⏺ Update(src/auth.ts) │ ┃ │ │ ⎿ Added 2 lines, removed 1 line │ ┃ │ │ ⏺ Bash(bun test) │ ┃ │ sempre ativos │ ⎿ 3 pass, 1 fail │ ┃ │ │ │ ┃ │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ │ [ Fechar ] │ │ ┃ ╰──────────────────────────────────────────╯ ✻ Worked for 42s · done 4:20 PM │ │ › /mods │ ⎿ meus-mods: Painel Meus Mods aberto. │ │ m: open-panel ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
m: open-panel
Pane · Meus Mods
╭──────────────────────────────────────────────────────────╮ │ ✦ MEUS MODS │ │ │ │ 0 de 0 mods ativos │ │ │ │ │ │ sempre ativos │ │ │ │ │ │ [ Fechar ] │ ╰──────────────────────────────────────────────────────────╯
README

claude-code-mods

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.

Mods

Security

ModDescription
guardaIntercepts risky Bash commands (rm -rf, git reset --hard, force push, new dependencies) and asks for confirmation explaining the impact.
lixeiraSends rm / Remove-Item targets to the Windows Recycle Bin instead of deleting them permanently.
cofreKeeps 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-guardBlocks commits that mix top-level directories, violate Conventional Commits, or lack a Co-Authored-By trailer.
universal-audit-logA hook on * that logs every event (tool calls, prompts, turns, fs/http/process calls from other plugins) with secret redaction.

Session & Context

ModDescription
context-monitorContext weather and token usage above the prompt, replay and activity panels, plain-language explanation before each edit, and ai-memory notes.
cache-frioConfirms before submitting a prompt to a large conversation whose prompt cache has gone cold.
session-telemetryTimestamp 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-inspectorSide pane displaying the session turn-by-turn: every model request and what it carried.
clean-viewCollapses each successful tool call into a single concise line (Edit file +12 −3); errors and in-flight calls retain the full block.
webfetch-cacheServes repeated WebFetch requests (identical URL and prompt) directly from the session cache.
reflexoDetects corrections and constraints in prompts, offering to save them as rules in CLAUDE.md.

Agents & Workflow

ModDescription
agent-flowLive diagram of agent flow: main loop context, subagent tree, and context passed/returned by each agent.
agent-radarOne live line per running subagent above the prompt; /radar opens a pane with messages from each.
delegation-dockStatus line indicator showing in-flight delegations (agy/codex/ollama) and alerting when one returns empty.
phase-trackerTracks and displays the current development phase (Spec → Red Test → Surgical Code → Verification).
next-promptsBand with 3 next-step suggestions generated by a free Ollama cloud model; click one to draft.
git-sidebarLazygit-style side pane: worktrees, local branches, branch switching, and worktree checkout.
wait-quizQuizzes a flashcard in a band above the prompt while Claude is working.
meus-modsCentral hub: quick-access bar above the prompt and /mods command displaying the status of each mod.

Installation

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.

Development

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).

Credits

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.

License

MIT

Source 1 files
hooks/register.tsx 296 lines
1import 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