Monte a árvore mínima de pastas
Um mod é uma pasta. O nome da pasta é livre. O que o Claude Code procura dentro dela é fixo: .claude-plugin/plugin.json e hooks/hooks.json.
O terceiro arquivo é o módulo, com a função register. Ele mora em hooks/, ao lado do hooks.json. Testes, tipos e README são extras: ajudam, mas o mod carrega sem eles.
🆕 Novo aqui? Manifesto e módulo
- Manifesto — o
plugin.json. Diz o nome do plugin, a versão e as opções. Sem ele, a pasta não é um plugin. - Módulo de hooks — o arquivo de código que exporta
register. Ohooks.jsonaponta para ele.
Como ler o desenho: os dois quadros verdes são os nomes que o Claude Code procura. A seta azul mostra que o hooks.json não tem código: ele só diz qual arquivo carregar. O quadro tracejado é o que você acrescenta para testar e tipar.
No terminal, na pasta onde você guarda projetos (uma linha de cada vez):
git clone https://github.com/inematds/claude-mods-starter-kit cd claude-mods-starter-kit
templates/starter-mod no editor. Têm de estar lá .claude-plugin/plugin.json, hooks/hooks.json, hooks/starter.mjs e tests/starter.test.ts. A pasta .claude-plugin começa com ponto: no gerenciador de arquivos, mostre os ocultos.quem é o plugin
qual módulo carregar
a função register
testes, tipos, README
Escreva o plugin.json
O manifesto do starter-mod tem quatro campos. O mais importante é name: ele identifica o plugin em todo lugar, do prefixo da saída de um comando à chave das opções guardadas.
Compare com o recibo-sessao, do kit INEMA. Ele acrescenta types (o contrato de tipos do estado) e userConfig (uma opção que aparece no /config).
{
"name": "read-counter-example",
"version": "0.1.0",
"description": "Teaching example: count successful Read events.",
"author": {
"name": "Prompt Advisers"
}
}
{
"name": "recibo-sessao",
"version": "0.1.0",
"description": "Recibo da sessão: lista o que o Claude criou ou alterou, com botão para abrir a pasta (/recibo, /recibo ultimo, /recibo limpar)",
"author": { "name": "INEMA", "email": "inematds@gmail.com" },
"types": "./types/index.d.ts",
"userConfig": {
"abridor": {
"type": "string",
"title": "Programa que abre pastas",
"description": "xdg-open no Linux, open no Mac, explorer no Windows.",
"default": "xdg-open",
"options": ["xdg-open", "open", "explorer"]
}
}
}
| Campo | Para quê | Onde você vê de novo |
|---|---|---|
name | identidade do plugin | saída do comando (read-counter-example: ...), pluginConfigs, instalação nome@marketplace |
version | versão publicada | atualização por marketplace (módulo 4.4) |
description, author | o que é e de quem é | listas de plugins |
types | aponta o contrato de tipos do mod | módulo 2.3 |
userConfig | opções que a pessoa escolhe | tópico 5 e /config |
O que olhar na tabela: só name aparece em três lugares diferentes. Trocar o nome depois muda a chave das opções guardadas e o nome de instalação. Escolha cedo (módulo 1.4, tópico 6).
✓ Bom nome
- ✓
contador-rafa: minúsculas e hífen - ✓ diz o que o mod faz
- ✓ único entre os plugins que você usa
✗ Nome que dá trabalho
- ✗ manter
read-counter-examplenuma cópia - ✗
Meu Mod, com espaço e maiúscula - ✗ trocar o nome depois de publicar
Aponte o módulo no hooks.json
O hooks/hooks.json tem uma chave só: modules, com um caminho. O caminho é relativo ao próprio hooks.json, não à raiz do plugin.
Por isso o starter-mod escreve ./starter.mjs, e não ./hooks/starter.mjs. O módulo apontado pode importar outros arquivos do mod com import normal; esses não entram no hooks.json.
{"modules":["./starter.mjs"]}
{ "modules": ["./register.tsx"] }
✓ Carrega
- ✓
"./starter.mjs"com o arquivo emhooks/ - ✓ módulo que faz
import { x } from "./util.mjs" - ✓ um módulo só, que pendura todos os hooks
✗ Não carrega
- ✗
"./hooks/starter.mjs"(virahooks/hooks/...) - ✗ módulo com
import()dinâmico - ✗ módulo sem
exportderegister
⚠️ import() derruba o módulo inteiro
A referência oficial é direta: um módulo que contém import() não carrega. Não adianta o import() estar num caminho que nunca roda. Use sempre import ... from "./arquivo" no topo.
em modules
ao hooks.json
traz o resto
nunca
Escolha a extensão do arquivo
O módulo e todo arquivo que ele importa precisam ter uma destas extensões: .ts, .tsx, .jsx, .js, .mjs, .cjs, .mts ou .cts. Arquivo com outro nome não é carregado.
E tem um detalhe que engana: é sempre ES module, qualquer que seja a extensão. Um .cjs aqui não vira CommonJS. Nada de require nem module.exports: use import e export.
| Extensão | Quem usa | Quando escolher |
|---|---|---|
.mjs | Starter Kit (starter.mjs) | primeiro mod, sem tipos, sem passo de compilação (trilha 1) |
.tsx | inema-mods (register.tsx) | mod tipado que desenha com JSX (trilhas 2 a 4) |
.ts | testes (*.test.ts) | mod tipado sem tela |
.js .jsx .cjs .mts .cts | aceitas | se o seu editor ou hábito pedir; o comportamento é o mesmo |
O que olhar na tabela: a escolha é sobre conforto, não sobre poder. O Claude Code lê TypeScript direto. Os tipos servem ao seu editor e ao tsc, não ao carregamento.
💡 Por que o curso começa em .mjs
Na trilha 1 você quer ver o mod carregar, sem brigar com tipos. Na trilha 2 o Rafa passa para .tsx com import type { Register } from 'claude-code', e o editor começa a apontar nome errado antes de rodar.
as outras não carregam
sempre
trilha 1
trilhas 2 a 4
Declare opções com userConfig
Cada campo de userConfig vira uma opção. O valor escolhido chega no segundo argumento de register(on, options), já com o padrão preenchido. No recibo-sessao, o mod lê options.abridor.
Um campo string com a lista options vira um seletor no /config, só com aqueles valores. Um valor guardado fora da lista conta como "não definido", e vale o default.
🆕 Novo aqui? pluginConfigs
É o bloco do settings.json onde o Claude Code guarda as escolhas, na chave pluginConfigs, com o name do plugin como chave (ou <name>@inline para um mod carregado por --plugin-dir). Campos sensíveis ficam no armazenamento seguro, não no arquivo, e não viram linha do menu.
Como ler o desenho: você escreve só a primeira caixa. O Claude Code cuida do seletor e da gravação. O mod nunca lê o settings.json: ele recebe o valor pronto em options, e recebe de novo quando a pessoa troca.
Declare o campo
type, title, description, default e, se for lista fechada, options com o default dentro dela.
Leia em register
Troque register(on) por register(on, options) e use options.abridor dentro dos hooks.
Teste com options
No teste, test(nome, { options }, corpo) entrega os valores como se viessem do settings.json (módulo 4.2).
no manifesto
seletor pronto
onde fica guardado
chega no register
Valide antes de carregar
O claude plugin validate lê o manifesto e o código do módulo do jeito que o engine vai ler, sem abrir sessão. Ele lista os eventos que o módulo pendura e os métodos do $ que chama.
É o teste mais barato que existe. Rode a cada mudança no manifesto ou no hooks.json. Se o evento que você escreveu não aparece na lista, o nome está errado.
Na raiz do kit (claude-mods-starter-kit):
claude plugin validate templates/starter-mod
Saída real no Claude Code 2.1.289 (o começo dos caminhos muda na sua máquina):
Validating plugin manifest: .../templates/starter-mod/.claude-plugin/plugin.json
Validating hooks: .../templates/starter-mod/hooks/hooks.json
❯ ./starter.mjs hooks: session.start, tool.call{tool=Read}, command.run{command=readcount}
❯ ./starter.mjs calls: $.command.register
✔ Validation passed
✔ Validation passed e a linha hooks: mostra os três eventos, com os matchers entre chaves. Se faltar um evento, confira o nome no código.⚠️ claude plugin init não cria um mod
O nome engana. O claude plugin init monta outro tipo de plugin, de hooks de comando, dentro de ~/.claude/skills. Um mod você escreve direto: os três arquivos deste módulo. Para começar rápido, copie o starter-mod (módulo 1.4).
Teste rápido (opcional): o Rafa pôs o módulo em hooks/contador.mjs e escreveu {"modules":["./hooks/contador.mjs"]} no hooks.json. O mod não carrega. Por quê?
sem abrir sessão
eventos e matchers
o que usa do $
outro tipo
🎓 Resumo do módulo
Próximo módulo:
1.3 — Carregar, recarregar e instalar