Pular para o conteúdo
MÓDULO 1.2

🗂️ Estrutura de arquivos e manifesto

O Claude Code só acha o seu mod se a pasta tiver a forma certa. Três arquivos bastam: o manifesto, o índice de módulos e o módulo. Aqui você monta cada um, entende cada campo e valida antes de carregar.

6
Tópicos
~35
Minutos
Base
Nível
Prático
Tipo
0 de 60%
1

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. O hooks.json aponta para ele.
starter-mod/ .claude-plugin/plugin.json manifesto: nome, versão, opções hooks/hooks.json hooks/starter.mjs módulo: export register tests/tsconfig.json verde = obrigatório · tracejado = extra

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.

🎯 Objetivo: ter a árvore do starter-mod na sua máquina

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
Como verificar: abra 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.
📇
plugin.json

quem é o plugin

🧭
hooks.json

qual módulo carregar

⚙️
módulo

a função register

🧪
extras

testes, tipos, README

2

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

📄 templates/starter-mod/.claude-plugin/plugin.json
{
  "name": "read-counter-example",
  "version": "0.1.0",
  "description": "Teaching example: count successful Read events.",
  "author": {
    "name": "Prompt Advisers"
  }
}
📄 inema-mods/mods/recibo-sessao/.claude-plugin/plugin.json
{
  "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"]
    }
  }
}
CampoPara quêOnde você vê de novo
nameidentidade do pluginsaída do comando (read-counter-example: ...), pluginConfigs, instalação nome@marketplace
versionversão publicadaatualização por marketplace (módulo 4.4)
description, authoro que é e de quem élistas de plugins
typesaponta o contrato de tipos do modmódulo 2.3
userConfigopções que a pessoa escolhetó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-example numa cópia
  • ✗ Meu Mod, com espaço e maiúscula
  • ✗ trocar o nome depois de publicar
3

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.

📄 templates/starter-mod/hooks/hooks.json
{"modules":["./starter.mjs"]}
📄 inema-mods/mods/recibo-sessao/hooks/hooks.json
{ "modules": ["./register.tsx"] }

✓ Carrega

  • ✓ "./starter.mjs" com o arquivo em hooks/
  • ✓ módulo que faz import { x } from "./util.mjs"
  • ✓ um módulo só, que pendura todos os hooks

✗ Não carrega

  • ✗ "./hooks/starter.mjs" (vira hooks/hooks/...)
  • ✗ módulo com import() dinâmico
  • ✗ módulo sem export de register

⚠️ 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.

1️⃣
um caminho

em modules

📍
relativo

ao hooks.json

🔗
import

traz o resto

🚫
import()

nunca

4

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ãoQuem usaQuando escolher
.mjsStarter Kit (starter.mjs)primeiro mod, sem tipos, sem passo de compilação (trilha 1)
.tsxinema-mods (register.tsx)mod tipado que desenha com JSX (trilhas 2 a 4)
.tstestes (*.test.ts)mod tipado sem tela
.js .jsx .cjs .mts .ctsaceitasse 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.

📜
8 extensões

as outras não carregam

📦
ES module

sempre

🟨
.mjs

trilha 1

🟦
.tsx

trilhas 2 a 4

5

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.

plugin.jsonuserConfig.abridordeclara /configseletor: xdg-openopen · explorer settings.jsonpluginConfigsguarda a escolha registeroptions.abridorrecarrega com o valor mudar no /config recarrega o módulo com as novas options

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.

1

Declare o campo

type, title, description, default e, se for lista fechada, options com o default dentro dela.

2

Leia em register

Troque register(on) por register(on, options) e use options.abridor dentro dos hooks.

3

Teste com options

No teste, test(nome, { options }, corpo) entrega os valores como se viessem do settings.json (módulo 4.2).

📝
userConfig

no manifesto

🎚️
/config

seletor pronto

🗄️
pluginConfigs

onde fica guardado

📥
options

chega no register

6

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.

🎯 Objetivo: ver o que o engine enxerga no starter-mod

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
Como verificar: a última linha é ✔ 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ê?

🔍
validate

sem abrir sessão

🪝
hooks:

eventos e matchers

💲
calls:

o que usa do $

🚫
plugin init

outro tipo

🎓 Resumo do módulo

✓
Três arquivos bastam — plugin.json, hooks.json e o módulo.
✓
name identifica o plugin — na saída, nas opções e na instalação.
✓
hooks.json tem um caminho — relativo a ele mesmo.
✓
Oito extensões, sempre ES module — e nunca import().
✓
userConfig vira /config e options — validate confere tudo antes.

Próximo módulo:

1.3 — Carregar, recarregar e instalar