◆ button on the mode line and /mods side pane to toggle mods and edit their settings live.

Mods para o Claude Code: plugins de function hooks que mudam a interface e o comportamento do próprio Claude Code.
| Mod | O que faz |
|---|---|
mods-hub | Botão ◆ mods na linha do mode e janela lateral para ligar, desligar e configurar os mods. |
status-inline | Medidores compactos (contexto, rate limits, modelo, custo, tokens) na linha do mode. |
model-picker | Troca o modelo e o effort da sessão pela janela de mods. |
<img width="1084" height="101" alt="image" src="https://github.com/user-attachments/assets/ac343e6d-1300-4469-9443-7b241f6e7b14" /> <img width="216" height="427" alt="image" src="https://github.com/user-attachments/assets/b03d8f45-afad-45ec-b53d-415e3e89bba8" />
Em qualquer sessão do Claude Code:
/plugin marketplace add Sales16/claude-mods
/plugin install mods-hub@claude-mods
/plugin install status-inline@claude-mods
/plugin install model-picker@claude-mods
mods-hub junto: o status-inline depende dele.Para pegar uma versão nova do repositório:
/plugin marketplace update claude-mods
/reload-plugins
Para remover:
/plugin uninstall model-picker@claude-mods
/plugin uninstall status-inline@claude-mods
/plugin uninstall mods-hub@claude-mods
Útil enquanto você mexe nos mods: o terminal vigia as pastas e recarrega o mod sozinho quando um arquivo muda.
Clone o repositório primeiro. Os exemplos abaixo usam C:\MyProjects\claude-mods; troque pelo caminho do seu clone.
git clone https://github.com/Sales16/claude-mods.git C:\MyProjects\claude-mods
Para todos os terminais, em ~/.claude/settings.json:
{
"env": {
"CLAUDE_CODE_PLUGIN_DIRS": "C:\\MyProjects\\claude-mods\\plugins\\mods-hub;C:\\MyProjects\\claude-mods\\plugins\\status-inline;C:\\MyProjects\\claude-mods\\plugins\\model-picker"
}
}
Para um terminal só:
claude --plugin-dir C:\MyProjects\claude-mods\plugins\mods-hub --plugin-dir C:\MyProjects\claude-mods\plugins\status-inline --plugin-dir C:\MyProjects\claude-mods\plugins\model-picker
Use um jeito só. Instalado e carregado pela pasta ao mesmo tempo, o mod roda duas vezes (dois ◆ mods na linha).
Cada opção dos mods fica salva no settings.json do usuário, em pluginConfigs, e vale entre sessões. O nome da chave muda com o jeito de carregar (status-inline@claude-mods instalado, status-inline@inline pela pasta): ao trocar de jeito, refaça as escolhas pelo ◆ mods.
Adiciona ◆ mods no começo da linha do mode:
◆ mods ⏵⏵ auto mode on │ ctx ▰▰▰▰▰▱▱▱ 62% │ …
Clicar em ◆ mods (ou rodar /mods) abre uma janela lateral com cada mod que tem configurações:
● model-picker
■ Ativo
▸ Modelo: opus
▸ Effort: high
● status-inline
■ Ativo
▾ Barra: ▰▱
● ▰▱
○ █░
■ ctx %
Amarelo a partir de % − 70 +
●/○ liga ou desliga o mod inteiro; ■/□ liga ou desliga cada opção.▸) abre a lista de valores embaixo dela; escolher um aplica na hora.− e +, de 5 em 5.Opção própria: Cor do ◆ mods (verde-neon, ciano, magenta, laranja, azul, roxo, branco).
Clique e hover dependem de o terminal enviar o mouse para o Claude Code, o que acontece no modo tela cheia. Sem mouse, use /mods, Tab e Enter.
Desenha medidores depois da linha do mode, em verde neon:
⏵⏵ auto mode on │ ctx ▰▰▰▰▰▱▱▱ 62% │ 5h ▰▱▱▱▱▱▱▱ 18% ↻14:30 │ 7d ▰▰▰▱▱▱▱▱ 41% ↻sex │ opus5.5·xh │ $1.23 │ tok 1.2M │ cmp ≈18k │ 🔥 →36%
| Medidor | O que mostra |
|---|---|
ctx | Quanto da janela de contexto o último request usou. |
5h | Uso da janela de rate limit de 5 horas e quando ela reseta. |
7d | Uso da janela semanal e o dia em que ela reseta. |
opus5.5·xh | Modelo e effort (lo, md, hi, xh, max) do request atual. |
$1.23 | Custo da sessão, como o /cost soma. |
tok 1.2M | Tokens gastos na sessão, conversa principal e subagents juntos, desde que o mod carregou. |
cmp ≈18k | Tokens que faltam até o auto-compact. |
🔥 →36% | Uso que a janela de 5h terá no reset, se o ritmo atual continuar. |
Configurações:
| Opção | Valores |
|---|---|
| Ativo | liga ou desliga os medidores |
| Cor | verde-neon, ciano, magenta, laranja, azul, roxo, branco |
| Barra | ▰▱, █░, ■□, ●○, ⣿⣀, ━┄ |
| Reset | oculto (18%), valor (18% 9:00), ícone (18% ↻9:00), texto (18% reset às 9:00) |
| Tokens | total, sem-cache (sem o cache relido), in/out (↑1.2M ↓40k), oculto |
| ctx, 5h, 7d, Modelo, Effort, Custo, cmp, Projeção 5h, Barras | mostra ou esconde cada item |
| Amarelo a partir de % | padrão 70 |
| Vermelho a partir de % | padrão 90 |
7d, modelo/effort. ctx e 5h nunca somem.◆ mods aberta ao lado, o mod conta a largura dela, porque a linha do mode passa por baixo das duas.Troca o modelo e o effort da sessão pela janela do ◆ mods.
| Opção | Valores |
|---|---|
| Ativo | liga ou desliga as trocas e a sincronização |
| Modelo | default, opus, sonnet, haiku, fable |
| Effort | low, medium, high, xhigh, max |
/model. Escolher um effort roda /effort <nível>, que executa quando a sessão fica parada./model, /effort ou troca de modelo pelo /config feita à mão passa a aparecer na janela. Um modelo como claude-opus-5-5 ou opus[1m] aparece como opus.claude-mods/
.claude-plugin/marketplace.json lista os mods para o /plugin install
plugins/
mods-hub/ botão ◆ mods, janela lateral, /mods
status-inline/ medidores na linha do mode
model-picker/ modelo e effort pela janela de mods
Cada mod tem .claude-plugin/plugin.json (nome, opções em userConfig), hooks/hooks.json e o módulo em hooks/register.ts(x), com seus testes em hooks/*.test.ts(x).
Um mod aparece na janela quando o plugin.json declara campos em userConfig.
enabled vira o liga/desliga do mod; o mod deve checar options.enabled e ficar inerte quando for false.string com options vira uma lista de valores.description do campo:Explicação do campo, uma ideia por linha.
Ex.: {#39FF14}ctx ▰▰▰▰▰▱▱▱ 62%{/}
-- <opção>
Explicação daquela opção.
Ex.: {#00F0FF}ctx ▰▰▰▰▰▱▱▱ 62%{/}
O Claude Code remove as quebras de linha do description, então o hub reencontra as linhas: uma linha começa em -- <opção> (uma das opções do campo), em Ex.: e depois de ., ! ou ? quando a frase seguinte começa com letra maiúscula. Comece toda linha assim e termine cada frase com ponto. Uma linha Ex.: é desenhada como exemplo, e {#RRGGBB}texto{/} pinta aquele trecho na cor.
No /config do Claude Code a mesma descrição aparece como texto puro, com as marcações cruas.
claude plugin validate plugins/<mod>
claude plugin test plugins/<mod>hooks/register.tsx 371 lines1import { atom, read, update } from 'claude-code'
2import type { ConfigRow, ConfigValue, EngineInterface, Register } from 'claude-code'
3
4const PANE = 'mods-hub'
5const TITLE = '◆ mods'
6const COLORS: Record<string, string> = {
7 'verde-neon': '#39FF14',
8 ciano: '#00F0FF',
9 magenta: '#FF2BD6',
10 laranja: '#FF9F1C',
11 azul: '#3D8BFF',
12 roxo: '#B266FF',
13 branco: '#F2F2F2',
14}
15const NUMBER_STEP = 5
16const HOVER_BACKGROUND = '#1E2A1E'
17
18const expanded = atom({ plugin: 'mods-hub', key: 'expanded' } as const, null)
19const dock = atom({ plugin: 'mods-hub', key: 'dock' } as const, 0)
20
21/** The docked pane's frame and divider, around its body's columns. */
22const DOCK_FRAME_COLUMNS = 3
23
24/** The `userConfig` field a mod declares to be switched on and off as a whole. */
25const ENABLED_FIELD = 'enabled'
26
27type ModGroup = { plugin: string; enabledRow?: ConfigRow; rows: ConfigRow[] }
28
29function groupByPlugin(rows: readonly ConfigRow[]): ModGroup[] {
30 const groups = new Map<string, ModGroup>()
31 for (const row of rows) {
32 const { plugin, tier } = row.provider
33 if (tier === 'core' || tier === 'builtin' || plugin === 'engine') continue
34
35 const group = groups.get(plugin) ?? { plugin, rows: [] }
36 if (row.key === `${plugin}.${ENABLED_FIELD}`) {
37 group.enabledRow = row
38 } else {
39 group.rows.push(row)
40 }
41 groups.set(plugin, group)
42 }
43
44 return [...groups.values()].sort((a, b) => a.plugin.localeCompare(b.plugin))
45}
46
47async function setRow($: EngineInterface, row: ConfigRow, value: ConfigValue): Promise<void> {
48 try {
49 const result = await $.config.set({ key: row.key, value })
50 if ('deny' in result && result.deny !== undefined) {
51 $.ui.toast(`${row.label}: ${result.deny}`)
52 }
53 } catch (error) {
54 $.ui.toast(`${row.label}: ${error instanceof Error ? error.message : String(error)}`)
55 }
56 $.ui.invalidate('ui.render')
57}
58
59type Help = { row: string[]; options: Record<string, string[]> }
60
61type Segment = { text: string; color?: string }
62
63/** Starts a line that shows an example of the row or option. */
64const EXAMPLE_PREFIX = 'Ex.: '
65
66function escapeRegExp(text: string): string {
67 return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
68}
69
70/**
71 * Splits a description into its lines. The engine hands a plugin field's
72 * description with its line breaks dropped, so a line is found again at each
73 * `-- <option>` (an option of the row, longest first), at each `Ex.: ` and
74 * after each sentence.
75 */
76export function descriptionLines(description: string, options: readonly string[] = []): string[] {
77 let text = description
78 if (!text.includes('\n')) {
79 const names = [...options].sort((a, b) => b.length - a.length).map(escapeRegExp)
80 if (names.length > 0) {
81 text = text.replace(new RegExp(`\\s*--\\s*(${names.join('|')})\\s*`, 'gu'), '\n-- $1\n')
82 }
83 text = text.replace(/\s*Ex\.:\s*/g, '\nEx.: ').replace(/([.!?])\s*(?=\p{Lu})/gu, '$1\n')
84 }
85
86 return text
87 .split('\n')
88 .map(line => line.trim())
89 .filter(line => line !== '')
90}
91
92/** A plugin's name without the marketplace it came from (`status-inline@inline`). */
93function displayName(plugin: string): string {
94 return plugin.replace(/@.*$/, '')
95}
96
97/**
98 * Reads a row's help from its description: the lines before the first
99 * `-- <option>` line explain the row, and each `-- <option>` line starts that
100 * option's own lines. A line led by `Ex.: ` is an example.
101 */
102function parseHelp(row: ConfigRow): Help {
103 const help: Help = { row: [], options: {} }
104 let lines = help.row
105 for (const line of descriptionLines(row.description ?? '', row.options)) {
106 const option = /^--\s+(.+)$/.exec(line)?.[1]?.trim()
107 if (option === undefined) {
108 if (line.trim() !== '') lines.push(line)
109 } else {
110 lines = help.options[option] = []
111 }
112 }
113
114 return help
115}
116
117/** Splits an example into its parts: `{#RRGGBB}text{/}` draws `text` in that color. */
118function exampleSegments(example: string): Segment[] {
119 const segments: Segment[] = []
120 let at = 0
121 for (const match of example.matchAll(/\{(#[0-9A-Fa-f]{6})\}(.*?)\{\/\}/g)) {
122 if (match.index > at) segments.push({ text: example.slice(at, match.index) })
123 segments.push({ text: match[2] ?? '', color: match[1] })
124 at = match.index + match[0].length
125 }
126 if (at < example.length) segments.push({ text: example.slice(at) })
127
128 return segments
129}
130
131function helpScope(key: string, option?: string): string {
132 return (option === undefined ? `h:${key}` : `h:${key}=${option}`).slice(0, 64)
133}
134
135async function choose($: EngineInterface, row: ConfigRow, value: string): Promise<void> {
136 await update($, expanded, () => null)
137 if (value !== row.value) await setRow($, row, value)
138}
139
140async function openPane($: EngineInterface): Promise<void> {
141 try {
142 await $.ui.open({ id: PANE, title: TITLE })
143 } catch (error) {
144 $.ui.toast(`mods: ${error instanceof Error ? error.message : String(error)}`)
145 }
146}
147
148export const register: Register = (on, options) => {
149 const NEON = COLORS[String(options['color'])] ?? '#39FF14'
150
151 on('session.start', async ($, e, next) => {
152 await $.command.register({
153 name: 'mods',
154 description: 'Open the mods pane: toggle mods and edit their settings',
155 })
156 const isShown = (await $.ui.panes()).some(pane => pane.id === PANE && pane.isShown)
157 if (!isShown) await update($, dock, () => 0)
158
159 return next(e)
160 })
161
162 on('ui.close', async ($, e, next) => {
163 const closed = await next(e)
164 if (e.id === PANE) await update($, dock, () => 0)
165
166 return closed
167 })
168
169 on('command.run', { command: 'mods' }, async $ => {
170 await openPane($)
171
172 return { text: 'Painel de mods aberto.' }
173 })
174
175 on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
176 const drawn = await next(e)
177 const { Box, Button, Text } = $.ui.resolve(e)
178
179 return (
180 <Box flexDirection="row">
181 <Button key="mods-hub-open" plain onPress={() => void openPane($)}>
182 <Text color={NEON}>◆ mods</Text>
183 </Button>
184 <Text> </Text>
185 {drawn}
186 </Box>
187 )
188 })
189
190 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
191 const { Box, Button, Text } = $.ui.resolve(e)
192 const groups = groupByPlugin(await $.config.list())
193 const openKey = await read($, expanded)
194
195 // A render never writes state: the docked width is published right after.
196 const docked = e.props.placement === 'dock' ? e.props.bodyColumns + DOCK_FRAME_COLUMNS : 0
197 if ((await read($, dock)) !== docked) {
198 $.clock.after(0, () => void update($, dock, () => docked))
199 }
200
201 if (groups.length === 0) {
202 return <Text dimColor>Nenhum mod com configurações carregado.</Text>
203 }
204
205 const helps: { scope: string; title: string; lines: string[] }[] = []
206
207 return (
208 <Box flexDirection="column">
209 {groups.map(group => {
210 const isOn = group.enabledRow === undefined || group.enabledRow.value !== false
211 if (group.enabledRow !== undefined) {
212 helps.push({
213 scope: helpScope(group.enabledRow.key),
214 title: displayName(group.plugin),
215 lines: parseHelp(group.enabledRow).row,
216 })
217 }
218
219 return (
220 <Box flexDirection="column" marginBottom={1}>
221 {group.enabledRow === undefined ? (
222 <Text color={NEON} bold>
223 ● {displayName(group.plugin)}
224 </Text>
225 ) : (
226 <Box hover={{ scope: helpScope(group.enabledRow.key), backgroundColor: HOVER_BACKGROUND }}>
227 <Button
228 key={`toggle:${group.plugin}`}
229 plain
230 onPress={() => {
231 const row = group.enabledRow
232 if (row !== undefined) void setRow($, row, !isOn)
233 }}
234 >
235 <Text color={isOn ? NEON : undefined} dimColor={!isOn} bold>
236 {isOn ? '●' : '○'} {displayName(group.plugin)}
237 </Text>
238 </Button>
239 </Box>
240 )}
241 {group.rows.map(row => {
242 const help = parseHelp(row)
243 const scope = helpScope(row.key)
244 helps.push({ scope, title: row.label, lines: help.row })
245
246 if (row.kind === 'boolean') {
247 const isChecked = row.value === true
248
249 return (
250 <Box hover={{ scope, backgroundColor: HOVER_BACKGROUND }}>
251 <Button key={`row:${row.key}`} plain onPress={() => void setRow($, row, !isChecked)}>
252 <Text color={isChecked && isOn ? NEON : undefined} dimColor={!isOn || !isChecked}>
253 {' '}
254 {isChecked ? '■' : '□'} {row.label}
255 </Text>
256 </Button>
257 </Box>
258 )
259 }
260
261 if (row.kind === 'number') {
262 const value = typeof row.value === 'number' ? row.value : 0
263
264 return (
265 <Box flexDirection="row" hover={{ scope, backgroundColor: HOVER_BACKGROUND }}>
266 <Text dimColor={!isOn}>
267 {' '}
268 {row.label}{' '}
269 </Text>
270 <Button key={`dec:${row.key}`} plain onPress={() => void setRow($, row, value - NUMBER_STEP)}>
271 <Text color={NEON}>−</Text>
272 </Button>
273 <Text color={NEON}> {value} </Text>
274 <Button key={`inc:${row.key}`} plain onPress={() => void setRow($, row, value + NUMBER_STEP)}>
275 <Text color={NEON}>+</Text>
276 </Button>
277 </Box>
278 )
279 }
280
281 if (row.kind === 'choice') {
282 const isOpen = openKey === row.key
283
284 return (
285 <Box flexDirection="column">
286 <Box hover={{ scope, backgroundColor: HOVER_BACKGROUND }}>
287 <Button
288 key={`row:${row.key}`}
289 plain
290 onPress={() => void update($, expanded, current => (current === row.key ? null : row.key))}
291 >
292 <Text dimColor={!isOn}>
293 {' '}
294 {isOpen ? '▾' : '▸'} {row.label}: <Text color={NEON}>{String(row.value)}</Text>
295 </Text>
296 </Button>
297 </Box>
298 {isOpen &&
299 (row.options ?? []).map(option => {
300 const isSelected = option === row.value
301 const optionScope = helpScope(row.key, option)
302 helps.push({
303 scope: optionScope,
304 title: `${row.label}: ${option}`,
305 lines: help.options[option] ?? help.row,
306 })
307
308 return (
309 <Box hover={{ scope: optionScope, backgroundColor: HOVER_BACKGROUND }}>
310 <Button key={`opt:${row.key}=${option}`} plain onPress={() => void choose($, row, option)}>
311 <Text color={isSelected ? NEON : undefined} dimColor={!isSelected}>
312 {' '}
313 {isSelected ? '●' : '○'} {option}
314 </Text>
315 </Button>
316 </Box>
317 )
318 })}
319 </Box>
320 )
321 }
322
323 return (
324 <Box hover={{ scope, backgroundColor: HOVER_BACKGROUND }}>
325 <Text dimColor>
326 {' '}
327 {row.label}: {String(row.value)}
328 </Text>
329 </Box>
330 )
331 })}
332 </Box>
333 )
334 })}
335 <Text dimColor>clique ou Tab + Enter · passe o mouse para ajuda · Esc fecha</Text>
336 {helps
337 .filter(help => help.lines.length > 0)
338 .map(help => (
339 <Box
340 flexDirection="column"
341 marginTop={1}
342 borderStyle="round"
343 borderColor={NEON}
344 paddingX={1}
345 display="none"
346 hover={{ scope: help.scope, display: 'flex' }}
347 >
348 <Text color={NEON} bold>
349 {help.title}
350 </Text>
351 {help.lines.map(line =>
352 line.startsWith(EXAMPLE_PREFIX) ? (
353 <Box flexDirection="row">
354 <Text dimColor>exemplo </Text>
355 <Text>
356 {exampleSegments(line.slice(EXAMPLE_PREFIX.length)).map(segment => (
357 <Text color={segment.color}>{segment.text}</Text>
358 ))}
359 </Text>
360 </Box>
361 ) : (
362 <Text>{line}</Text>
363 ),
364 )}
365 </Box>
366 ))}
367 </Box>
368 )
369 })
370}
371types/index.d.ts 16 lines1/** The `/config` key of the choice row whose options are listed open, if any. */
2export type ModsHubExpanded = string | null
3
4/**
5 * Columns the mods pane takes from the transcript while docked beside it; 0
6 * while it is closed or inline. The mode line spans both, so a mod drawing
7 * there adds this to its viewport's columns.
8 */
9export type ModsHubDock = number
10
11declare module 'claude-code' {
12 interface PluginState {
13 'mods-hub': { expanded: ModsHubExpanded; dock: ModsHubDock }
14 }
15}
16