While Claude works, a band above the prompt quizzes one card from configured decks (Anki JSON, markdown deck, Linux) or cards captured with /cartao; never…

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 190 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Card } from '../types'
5import { LINUX } from './linux'
6
7// Optional external deck file paths, configurable via environment variables:
8// WAIT_QUIZ_ANKI_PATH (e.g. an anki JSON export)
9// WAIT_QUIZ_DECK_PATH (e.g. a markdown flashcard deck)
10// Captured cards live in the plugin's own $.store, never in the source decks.
11const MAX = 280
12const CAP_KEY = 'captured'
13const CAP_MAX = 500
14
15const card = atom({ plugin: 'wait-quiz', key: 'card' } as const, null)
16const isRevealed = atom({ plugin: 'wait-quiz', key: 'isRevealed' } as const, false)
17const isEnabledRef = atom({ plugin: 'wait-quiz', key: 'isEnabled' } as const, true)
18
19const clean = (s: string) => {
20 const t = s.replace(/<br\s*\/?>/gi, ' / ').replace(/<[^>]+>/g, '').replace(/\*\*|`/g, '').replace(/ /g, ' ').replace(/\s+/g, ' ').trim()
21 return t.length > MAX ? `${t.slice(0, MAX - 1)}…` : t
22}
23
24/** anki_export.json: `[[id, pt, it], ...]`; asks the Italian for the Portuguese. */
25export function parseAnki(raw: string): Card[] {
26 const rows = JSON.parse(raw) as unknown
27 if (!Array.isArray(rows)) return []
28 return rows.flatMap(r =>
29 Array.isArray(r) && typeof r[1] === 'string' && typeof r[2] === 'string' && clean(r[1]) && clean(r[2])
30 ? [{ src: 'it' as const, front: clean(r[1]), back: clean(r[2]) }]
31 : [],
32 )
33}
34
35/** RoadToBSCoDev deck.md: table rows `| id | front | back | topic | ...`; header and separator rows have no numeric id. */
36export function parseDeck(raw: string): Card[] {
37 return raw.split('\n').flatMap(line => {
38 // ponytail: plain split on `|`; the deck has no escaped pipes today. Split on /(?<!\\)\|/ if one appears.
39 const [, id, front, back, topic] = line.split('|').map(c => c.trim())
40 return id && /^\d+$/.test(id) && front && back ? [{ src: 'dev' as const, front: clean(front), back: clean(back), ...(topic ? { topic } : {}) }] : []
41 })
42}
43
44// ponytail: decks held in module memory, re-read on reload; files are a few MB at most.
45let decks: Card[][] | undefined
46
47async function loadDecks($: EngineInterface): Promise<Card[][]> {
48 if (decks) return decks
49 const readOr = async (path: string | undefined) => {
50 if (!path) return ''
51 return $.fs.read(path).catch(() => '')
52 }
53 const ankiPath = await $.env.get('WAIT_QUIZ_ANKI_PATH').catch(() => undefined)
54 const deckPath = await $.env.get('WAIT_QUIZ_DECK_PATH').catch(() => undefined)
55 const [it, dev] = await Promise.all([readOr(ankiPath), readOr(deckPath)])
56 const safe = (f: () => Card[]) => {
57 try {
58 return f()
59 } catch {
60 return []
61 }
62 }
63 decks = [safe(() => parseAnki(it)), safe(() => parseDeck(dev)), LINUX].filter(d => d.length > 0)
64 return decks
65}
66
67/** One deck at a time, evenly: the anki deck is ~200x bigger than the others and would drown them. */
68export function pick(ds: Card[][], rnd: () => number = Math.random): Card | null {
69 if (!ds.length) return null
70 const deck = ds[Math.floor(rnd() * ds.length)]!
71 return deck[Math.floor(rnd() * deck.length)] ?? null
72}
73
74async function captured($: EngineInterface): Promise<Card[]> {
75 const v = await $.store.get(CAP_KEY).catch(() => undefined)
76 return Array.isArray(v) ? (v as Card[]) : []
77}
78
79/** Validates and appends one card to the captured deck; the reply is what the person or the model reads. */
80export function addCard(deck: readonly Card[], raw: { front?: unknown; back?: unknown; topic?: unknown }): { deck: Card[]; reply: string } {
81 const front = typeof raw.front === 'string' ? raw.front.trim() : ''
82 const back = typeof raw.back === 'string' ? raw.back.trim() : ''
83 const topic = typeof raw.topic === 'string' ? raw.topic.trim().slice(0, 40) : ''
84 if (!front || !back) return { deck: [...deck], reply: 'Cartão não salvo: frente e verso não podem ficar vazios.' }
85 if (front.length > MAX || back.length > MAX) return { deck: [...deck], reply: `Cartão não salvo: frente e verso têm no máximo ${MAX} caracteres cada.` }
86 if (deck.some(c => c.front.toLowerCase() === front.toLowerCase())) return { deck: [...deck], reply: `Já existe um cartão com essa frente: "${front}".` }
87 // ponytail: oldest dropped past CAP_MAX, keeps the store far under its 4 MiB cap.
88 const next = [...deck, { src: 'cap' as const, front, back, ...(topic ? { topic } : {}) }].slice(-CAP_MAX)
89 return { deck: next, reply: `📥 Cartão salvo em Capturados (${next.length} no deck): "${front}"` }
90}
91
92async function save($: EngineInterface, raw: { front?: unknown; back?: unknown; topic?: unknown }): Promise<string> {
93 const { deck, reply } = addCard(await captured($), raw)
94 await $.store.set(CAP_KEY, deck)
95 return reply
96}
97
98/** `/cartao frente | verso | tópico`, the topic optional. */
99export function parseCommand(args: string): { front?: string; back?: string; topic?: string } {
100 const [front, back, topic] = args.split('|').map(s => s.trim())
101 return { front, back, topic }
102}
103
104async function deal($: EngineInterface) {
105 const cap = await captured($)
106 const next = pick([...(await loadDecks($)), ...(cap.length ? [cap] : [])])
107 await update($, card, () => next)
108 await update($, isRevealed, () => false)
109}
110
111const NAME = { it: '🇮🇹 PT→IT', dev: '💻 RoadToDev', linux: '🐧 Linux', cap: '📥 Capturados' } as const
112const COLOR = { it: 'green', dev: 'cyan', linux: 'yellow', cap: 'magenta' } as const
113const label = (c: Card) => `${NAME[c.src]}${c.topic ? ` · ${c.topic}` : ''}`
114
115export const register: Register = on => {
116 on('session.start', async ($, e, next) => {
117 const saved = await $.store.get('isEnabled').catch(() => undefined)
118 await update($, isEnabledRef, () => (typeof saved === 'boolean' ? saved : isEnabledRef.initial))
119 await $.command.register({ name: 'cartao', description: 'Salva um cartão de estudo: /cartao frente | verso | tópico' })
120 await $.tool.register({
121 name: 'salvar_cartao',
122 description:
123 'Salva um cartão de estudo no deck "Capturados" do usuário, que aparece no quiz enquanto Claude trabalha. Use quando o usuário pedir para guardar algo, ou logo depois de explicar um conceito técnico que ele claramente não conhecia (no máximo um por resposta). Frente: uma pergunta curta. Verso: a resposta em português simples. Até 280 caracteres cada.',
124 inputSchema: {
125 type: 'object',
126 properties: {
127 front: { type: 'string', description: 'A pergunta' },
128 back: { type: 'string', description: 'A resposta, em português simples' },
129 topic: { type: 'string', description: 'Assunto curto, ex.: git, linux, react' },
130 },
131 required: ['front', 'back'],
132 },
133 isDeferred: false,
134 })
135 await $.command.register({
136 name: 'wait-quiz',
137 description: 'Liga/desliga o quiz na faixa acima do prompt enquanto Claude trabalha.',
138 argumentHint: 'on|off',
139 })
140 return next(e)
141 }).catch(($, e, next) => next(e))
142
143 on('command.run', { command: 'wait-quiz' }, async ($, e) => {
144 const arg = e.args.trim().toLowerCase()
145 if (arg !== 'on' && arg !== 'off') {
146 const isEnabled = await read($, isEnabledRef)
147 return { text: `wait-quiz esta ${isEnabled ? 'ativado' : 'desativado'}. Uso: /wait-quiz on · /wait-quiz off` }
148 }
149 await update($, isEnabledRef, () => arg === 'on')
150 await $.store.set('isEnabled', arg === 'on') // $.state is per session; the store survives a restart
151 return { text: `wait-quiz ${arg === 'on' ? 'ativado' : 'desativado'}.` }
152 })
153
154 on('command.run', { command: 'cartao' }, async ($, e) => {
155 if (!e.args.trim()) return { text: `Uso: /cartao frente | verso | tópico. Capturados até agora: ${(await captured($)).length}.` }
156 return { text: await save($, parseCommand(e.args)) }
157 })
158
159 on('tool.call', { tool: 'mcp__wait-quiz__salvar_cartao' }, async ($, e) => {
160 const reply = await save($, e as { front?: unknown; back?: unknown; topic?: unknown }).catch(() => 'Cartão não salvo: falha ao gravar.')
161 return { result: reply }
162 })
163
164 on('prompt.submit', async ($, e, next) => {
165 if ((e.origin.kind === 'composer' || e.origin.kind === 'bridge') && (await read($, isEnabledRef))) await deal($).catch(() => undefined)
166 return next(e)
167 }).catch(($, e, next) => next(e)) // a quiz bug must never eat the prompt
168
169 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
170 const c = await read($, card)
171 if (!e.props.isWorking || e.props.hasSurvey || !c || !(await read($, isEnabledRef))) return next(e)
172 const revealed = await read($, isRevealed)
173 const { Box, Button, Text } = $.ui.resolve(e)
174 return (
175 <Box flexDirection="column" borderStyle="round" borderColor={COLOR[c.src]} paddingX={1}>
176 <Text>
177 <Text bold color={COLOR[c.src]}>
178 {label(c)}
179 </Text> {c.front}
180 </Text>
181 {revealed ? <Text color="green">→ {c.back}</Text> : null}
182 <Box>
183 {revealed ? null : <Button key="reveal" label="Revelar" onPress={() => update($, isRevealed, () => true)} />}
184 <Button key="next" label="Outra" onPress={() => deal($)} />
185 </Box>
186 </Box>
187 )
188 })
189}
190hooks/linux.ts 64 lines1import type { Card } from '../types'
2
3/** Own Linux deck, aligned with RoadToBSCoDev stage 01 (terminal, OS, PATH). Edit freely: one [topic, front, back] per card. */
4const CARDS: readonly (readonly [topic: string, front: string, back: string])[] = [
5 // navegação
6 ['navegacao', 'Qual a diferença entre caminho absoluto e relativo?', 'Absoluto começa na raiz `/` (ex.: /home/bruno/dev) e vale de qualquer lugar. Relativo parte do diretório atual (ex.: dev/notas).'],
7 ['navegacao', 'O que significam `.`, `..` e `~` num caminho?', '`.` = diretório atual; `..` = diretório pai; `~` = seu diretório home.'],
8 ['navegacao', 'Qual comando mostra em que diretório você está?', '`pwd` (print working directory).'],
9 ['navegacao', 'O que faz `cd -`?', 'Volta para o diretório anterior (alterna entre os dois últimos).'],
10 ['navegacao', 'Por que `ls` não mostra alguns arquivos, e como vê-los?', 'Arquivos que começam com `.` são ocultos. `ls -a` mostra; `ls -la` mostra ocultos com permissões, dono, tamanho e data.'],
11 ['navegacao', 'Como criar `a/b/c` de uma vez, mesmo se `a` não existir?', '`mkdir -p a/b/c` — o `-p` cria os diretórios intermediários e não reclama se já existirem.'],
12 ['navegacao', 'Qual tecla autocompleta nomes de arquivos e comandos no terminal?', 'Tab. Duas vezes Tab lista as opções quando há mais de uma.'],
13 ['navegacao', 'Como buscar um comando que você digitou antes?', 'Ctrl+R faz busca reversa no histórico; `history` lista tudo.'],
14 // arquivos
15 ['arquivos', 'O `rm` manda o arquivo para a lixeira?', 'Não. `rm` apaga direto, sem lixeira. `rm -r` apaga diretórios recursivamente — confira o caminho antes.'],
16 ['arquivos', 'Qual a diferença entre `cp` e `mv`?', '`cp` copia (o original fica); `mv` move ou renomeia (o original some). Para copiar diretório: `cp -r`.'],
17 ['arquivos', 'O que faz `touch arquivo.txt`?', 'Cria o arquivo vazio se não existir; se existir, só atualiza a data de modificação.'],
18 ['arquivos', 'Como ver as últimas linhas de um log e continuar acompanhando o que chega?', '`tail -f arquivo.log`. Sem `-f`, `tail -n 20` mostra só as últimas 20 linhas.'],
19 ['arquivos', 'Por que usar `less` em vez de `cat` para um arquivo grande?', '`cat` despeja tudo na tela; `less` pagina, permite rolar e buscar com `/` e sair com `q`.'],
20 ['arquivos', 'O que é um link simbólico e como criar?', 'Um atalho que aponta para outro caminho: `ln -s alvo nome_do_link`. Apagar o link não apaga o alvo.'],
21 ['arquivos', 'Como achar todos os `.log` abaixo do diretório atual?', '`find . -name "*.log"` — as aspas impedem o shell de expandir o `*` antes.'],
22 ['arquivos', 'Como procurar um texto dentro de arquivos de uma pasta?', '`grep -rn "texto" pasta/` — `-r` recursivo, `-n` mostra o número da linha, `-i` ignora maiúsculas.'],
23 // permissões
24 ['permissoes', 'Como ler `-rwxr-xr--`?', 'Primeiro caractere = tipo (`-` arquivo, `d` diretório). Depois 3 trios: dono `rwx`, grupo `r-x`, outros `r--`.'],
25 ['permissoes', 'Quanto vale r, w e x em octal?', 'r=4, w=2, x=1. Soma por trio: 7=rwx, 6=rw-, 5=r-x, 4=r--.'],
26 ['permissoes', 'O que dá `chmod 755` e `chmod 644`?', '755 = rwxr-xr-x (executáveis e diretórios). 644 = rw-r--r-- (arquivos comuns).'],
27 ['permissoes', 'Como tornar um script executável só para o dono?', '`chmod u+x script.sh` (u=dono, g=grupo, o=outros, a=todos).'],
28 ['permissoes', 'O que a permissão `x` significa num diretório?', 'Poder entrar/atravessar o diretório (`cd` e acessar o que tem dentro). Sem `x`, nem com `r` você abre os arquivos.'],
29 ['permissoes', 'Para que servem `sudo` e `chown`?', '`sudo` roda um comando como root. `chown usuario:grupo arquivo` troca o dono do arquivo.'],
30 // pipes e redirecionamento
31 ['pipes', 'Quais são os três fluxos padrão de um processo e seus números?', 'stdin = 0 (entrada), stdout = 1 (saída), stderr = 2 (erros).'],
32 ['pipes', 'O que o `|` (pipe) faz?', 'Liga o stdout do comando da esquerda ao stdin do da direita. Ex.: `ls | wc -l` conta as entradas.'],
33 ['pipes', 'Diferença entre `>` e `>>`?', '`>` sobrescreve o arquivo; `>>` anexa no final.'],
34 ['pipes', 'Como mandar stdout e stderr para o mesmo arquivo?', '`cmd > saida.txt 2>&1` — a ordem importa: primeiro stdout vai pro arquivo, depois stderr segue o stdout.'],
35 ['pipes', 'Para que serve `/dev/null`?', 'Um "buraco negro": o que é escrito nele é descartado. Ex.: `cmd 2>/dev/null` esconde os erros.'],
36 ['pipes', 'Diferença entre `&&`, `||` e `;` entre comandos?', '`a && b` roda b só se a deu certo; `a || b` só se a falhou; `a ; b` roda b sempre.'],
37 ['pipes', 'O que é o exit code e como vê-lo?', 'Número que o processo devolve ao terminar: 0 = sucesso, outro = erro. `echo $?` mostra o do último comando.'],
38 // processos
39 ['processos', 'Diferença entre processo e thread?', 'Processo tem espaço de memória próprio e isolado. Threads vivem dentro de um processo e compartilham a memória dele.'],
40 ['processos', 'Como listar os processos em execução?', '`ps aux` tira uma foto; `top` ou `htop` mostram ao vivo, com CPU e memória.'],
41 ['processos', 'O que `kill PID` faz por padrão?', 'Envia SIGTERM (15): pede para o processo terminar, e ele pode se limpar antes de sair.'],
42 ['processos', 'Quando usar `kill -9` e por que é o último recurso?', 'Envia SIGKILL: o kernel mata na hora e o processo não consegue capturar nem limpar nada (arquivos temporários, conexões).'],
43 ['processos', 'O que Ctrl+C e Ctrl+Z fazem com o processo em primeiro plano?', 'Ctrl+C envia SIGINT (interrompe). Ctrl+Z suspende (SIGTSTP); depois `bg` continua em segundo plano e `fg` traz de volta.'],
44 ['processos', 'Como rodar um comando em segundo plano?', 'Coloque `&` no final: `comando &`. `jobs` lista os que estão rodando nesse terminal.'],
45 ['processos', 'Qual é o PID 1 num Linux moderno?', 'O init — na maioria das distros, o systemd. É o ancestral de todos os outros processos.'],
46 ['processos', 'Como um novo processo nasce no Linux (fork/exec)?', '`fork` cria uma cópia do processo atual; `exec` troca o programa dessa cópia pelo novo. O shell faz isso a cada comando.'],
47 // kernel
48 ['kernel', 'Diferença entre kernel space e user space?', 'Kernel space roda com acesso total ao hardware; user space é onde ficam os programas, sem acesso direto. Um bug no kernel derruba a máquina; num programa, só o programa.'],
49 ['kernel', 'O que é uma syscall?', 'A porta do user space para o kernel: o programa pede um serviço (open, read, write, fork) e o kernel executa por ele.'],
50 ['kernel', 'O que quer dizer "no Linux, tudo é arquivo"?', 'Dispositivos (/dev), informação de processos (/proc), sockets e pipes são acessados pela mesma interface de arquivo: abrir, ler, escrever.'],
51 ['kernel', 'Para que servem /etc, /var/log, /tmp e /home?', '/etc = configurações; /var/log = logs; /tmp = temporários (podem sumir no boot); /home = diretórios dos usuários.'],
52 // env
53 ['env', 'O que é a variável PATH?', 'Lista de diretórios, separados por `:`, onde o shell procura o executável quando você digita um comando.'],
54 ['env', 'Por que se roda `./script.sh` e não só `script.sh`?', 'O diretório atual não está no PATH (por segurança). `./` diz o caminho explícito.'],
55 ['env', 'Como saber qual binário roda quando você digita `python`?', '`which python` (ou `type python`) mostra o caminho encontrado no PATH.'],
56 ['env', 'Diferença entre `VAR=x` e `export VAR=x`?', 'Sem export, a variável só existe no shell atual. Com export, ela é herdada pelos processos filhos que o shell iniciar.'],
57 ['env', 'Como listar e ler variáveis de ambiente?', '`env` ou `printenv` listam todas; `echo $HOME` lê uma.'],
58 ['env', 'Onde colocar um `export` para valer em todo terminal novo?', 'No `~/.bashrc` (bash) ou `~/.zshrc` (zsh). Para aplicar no terminal atual: `source ~/.bashrc`.'],
59 ['env', 'Para que serve a primeira linha `#!/usr/bin/env bash` de um script?', 'É o shebang: diz qual interpretador roda o arquivo. O `env` acha o bash pelo PATH.'],
60 ['env', 'Onde ler a documentação de um comando no próprio terminal?', '`man comando` abre o manual; `comando --help` dá o resumo.'],
61]
62
63export const LINUX: Card[] = CARDS.map(([topic, front, back]) => ({ src: 'linux', front, back, topic }))
64types/index.d.ts 8 lines1export type Card = { src: 'it' | 'dev' | 'linux' | 'cap'; front: string; back: string; topic?: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'wait-quiz': { card: Card | null; isRevealed: boolean; isEnabled: boolean }
6 }
7}
8