Veja a variável de módulo sumir
O starter-mod do módulo 1.4 guarda o contador numa variável: let count = 0, dentro do register. Funciona, até você salvar o arquivo. No reload, o register roda de novo num ambiente novo, e a variável nasce zerada.
Isso não é defeito. É o jeito de o código novo entrar sem reiniciar a sessão. Mas tudo o que estava só na memória do módulo vai embora junto: variáveis, timers, caches. Vale a pena ver acontecer uma vez.
🆕 Novo aqui? Hot-reload e variável de módulo
Hot-reload é a recarga do mod quando você salva um arquivo da pasta vigiada (módulo 1.3). Variável de módulo é qualquer let ou const que vive no arquivo do mod ou dentro do register: ela existe só enquanto aquele ambiente existir.
Na pasta do Claude Mods Starter Kit (da Prompt Advisers, licença MIT), abra uma sessão com o mod carregado da pasta:
claude --plugin-dir templates/starter-mod
Dentro da sessão, um passo de cada vez:
Leia o README.md e me diga o título. /readcount (no editor: acrescente um espaço no comentário da 1ª linha de templates/starter-mod/hooks/starter.mjs e salve) /readcount
/readcount mostra pelo menos uma leitura. Depois de salvar, o segundo /readcount volta para 0 (resultado esperado, não medido nesta máquina). Se não voltar, confira se a sessão foi aberta com --plugin-dir: só pasta vigiada recarrega.✓ Pode ficar em variável
- ✓ Constantes e opções lidas do
options - ✓ Contas de um turno só, como o
startedAte otoolsdoband.tsx - ✓ Algo que, se zerar, ninguém nota
✗ Não pode ficar em variável
- ✗ O que a tela desenha
- ✗ Listas que crescem na sessão, como o recibo
- ✗ Preferências da pessoa (ligado/desligado)
Declare o contrato em types/index.d.ts
O lugar certo para o que precisa sobreviver ao reload é o $.state: valores que o host guarda durante a sessão, cada um com nome e versão. Antes de usar, você declara o formato num arquivo de tipos do mod.
Esse arquivo é o contrato. Ele diz ao TypeScript, ao claude plugin validate e a outros mods quais chaves o seu mod guarda e de que tipo. O plugin.json aponta para ele com "types".
🆕 Novo aqui? PluginState
PluginState é uma interface do módulo 'claude-code' que cada mod amplia com declare module. A chave de fora é o nome do mod; dentro dela, cada chave é um valor guardado.
export type Entrada = {
caminho: string
acao: 'criado' | 'editado'
turno: number
hora: number
}
declare module 'claude-code' {
interface PluginState {
'recibo-sessao': { entradas: Entrada[]; turno: number }
}
}
E a linha no .claude-plugin/plugin.json do mesmo mod:
"types": "./types/index.d.ts",
Entrada) e o item do mod ('recibo-sessao': { ... }) é um objeto escrito ali mesmo. As duas coisas são exigências, não estilo.⚠️ Duas pegadinhas do kit INEMA
13: o contrato precisa exportar ao menos um tipo; um export {} vazio faz o validate falhar. 21: em PluginState, o item do mod tem de ser um objeto literal; apontar para um tipo com nome ('recibo-sessao': MeuEstado) dá "not declared".
Na pasta do inema-mods, troque <caminho do tsc> pelo tsc da sua máquina:
TSC=<caminho do tsc> scripts/checar-mod.sh mods/recibo-sessao
Saída real (05/10/2026):
== recibo-sessao validate: ok tsc: ok test: ok (6 linhas de aprovação)
ok. A linha tsc é a que confere o contrato: se você mudar turno: number para turno: string no index.d.ts, ela deixa de dar ok. Desfaça a mudança depois do teste.Use atom, read e update
Dá para usar $.state.get e $.state.set direto. Mas o próprio 'claude-code' traz três funções que deixam o código curto e seguro: atom, read e update. O exemplo oficial da faixa usa as três.
Repare no desenho do arquivo: o que a tela mostra (last, isHidden) está em átomos. As contas do turno (startedAt, tools) estão em variáveis, porque perder isso num reload não faz falta.
🆕 Novo aqui? As três funções
atom(ref, inicial)— um valor com nome ({ plugin, key }, os dois literais) e valor inicial. Declare no topo do arquivo.read($, átomo)— lê o valor; dentro de umui.render, também inscreve aquele desenho para ser refeito quando o valor mudar.update($, átomo, fn)— lê, aplicafne grava com checagem de versão; se outro escreveu no meio, tenta de novo.
import { atom, read, update } from 'claude-code'
import type { Register } from 'claude-code'
import type { Turn } from '../types'
const last = atom({ plugin: 'turn-band', key: 'last' } as const, null)
const isHidden = atom({ plugin: 'turn-band', key: 'isHidden' } as const, false)
export const register: Register = on => {
let startedAt = 0
let tools = 0
on('prompt.submit', async ($, e, next) => {
startedAt = await $.clock.now()
tools = 0
return next(e)
})
on('tool.call', ($, e, next) => {
tools += 1
return next(e)
})
on('turn.complete', async ($, e, next) => {
const seconds = Math.round(((await $.clock.now()) - startedAt) / 1000)
const turn: Turn = { seconds, tools }
await update($, last, () => turn)
if (seconds > 120) {
$.ui.toast(`That turn took ${seconds}s`)
}
return next(e)
})
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
const turn = await read($, last)
const isQuiet = e.props.hasSurvey || turn === null || (await read($, isHidden))
if (isQuiet) {
return next(e)
}
const { Box, Button, Text } = $.ui.resolve(e)
return (
<Box>
<Text dimColor>
Last turn: {turn.seconds}s, {turn.tools} tool calls{' '}
</Text>
<Button
key="hide"
label="Hide"
onPress={() => update($, isHidden, () => true)}
/>
</Box>
)
})
}
turn.complete escreve com update; o ui.render só lê com read; o botão escreve de dentro do onPress. Ninguém chama $.ui.invalidate: a leitura no desenho já inscreve a faixa. O contrato está em examples/band-state.d.ts, igual ao do recibo.Declare o átomo no topo
atom({ plugin, key } as const, inicial), com plugin e key literais, iguais ao contrato.
Escreva num evento
update($, last, () => turn) no turn.complete.
Leia no desenho
read($, last) no ui.render inscreve a faixa naquele valor.
Escreva num handler
O onPress chama update; o host redesenha quem leu, sem $.ui.invalidate.
Como ler o desenho: as setas azuis da esquerda são escritas, vindas de eventos ou de botões. A seta da direita é leitura. A linha vermelha é proibida: um $.state.set durante o desenho é negado pelo engine. Escreva sempre de um evento ou de um handler.
Guarde entre sessões com $.store
O $.state sobrevive ao reload, mas é da sessão. Fechou o Claude Code, acabou. Para durar entre sessões existe o $.store: um arquivo JSON só do seu mod, na pasta de configuração do Claude Code da pessoa.
O padrão do kit INEMA junta os dois: no session.start, lê do $.store e põe no átomo; quando a pessoa muda a preferência, grava nos dois. O desenho continua lendo só do átomo.
async function carregarLigado($: EngineInterface) {
const salvo = await $.store.get(CHAVE_LIGADO).catch(() => undefined)
await update($, ligado, () => salvo !== false)
}
// ... no command.run do /bichinho:
if (arg === 'off' || arg === 'on') {
const valor = arg === 'on'
await update($, ligado, () => valor)
await $.store.set(CHAVE_LIGADO, valor)
return { text: valor ? 'Bichinho acordado.' : 'Bichinho foi dormir (use /bichinho on para acordar).' }
}
carregarLigado está no topo do arquivo e é chamada no session.start. Se não há nada salvo, o padrão é ligado (salvo !== false). O .catch evita que um store ilegível derrube o início da sessão.| Método | Faz | Detalhe dos tipos |
|---|---|---|
$.store.get(chave) | lê | undefined se nunca gravou |
$.store.set(chave, valor) | grava | só JSON; até 4 MiB no total |
$.store.delete(chave) | apaga | — |
$.store.keys() | lista as chaves | na ordem em que foram criadas |
💡 O que volta não é o que foi
O get devolve o valor depois de passar por JSON: uma Date volta como texto ISO, um Map ou Set volta como {}, campo undefined some. Guarde números e textos simples. E lembre: o que você grava testando ao vivo fica gravado; volte ao padrão depois.
Use o relógio do engine
Muito estado tem hora: quando o arquivo foi editado, quanto o turno durou. A tentação é Date.now(). Não use. A hora vem de await $.clock.now(), que devolve os milissegundos do relógio do engine.
O motivo aparece no teste (módulo 4.2): o claude plugin test controla esse relógio. Ele avança o tempo na hora que o teste quer, e o resultado é sempre o mesmo. Com Date.now(), o teste depende da hora em que roda. É a pegadinha 7 do kit INEMA.
async function registrar($: EngineInterface, caminho: string, acao: Entrada['acao']) {
const n = await read($, turno)
const hora = await $.clock.now()
await update($, entradas, lista => [...lista, { caminho, acao, turno: n, hora }].slice(-500))
}
$ e está no topo. A hora vem do engine. E a lista tem teto (.slice(-500)), para o estado não crescer sem fim numa sessão longa.✓ Faça
- ✓
const agora = await $.clock.now() - ✓ Guardar a hora como número no estado
- ✓ Converter para texto só na hora de mostrar
✗ Evite
- ✗
Date.now()ounew Date()sem argumento - ✗ Guardar
Dateno$.store(volta como texto) - ✗ Testar data perto da meia-noite: use meio-dia UTC (pegadinha 23)
Escolha onde cada dado mora
Agora são três lugares, e cada um dura um tempo. A escolha é uma pergunta só: se isso sumir, quem percebe? Ninguém: variável. A tela: $.state. A pessoa amanhã: $.store, com cópia no $.state se a tela também mostra.
Como ler o desenho: cada barra é quanto um dado vive. A cinza morre no reload (amarelo). A azul passa pelo reload e morre no fim da sessão (vermelho). Só a última chega à sessão 2.
| Dado do Rafa | Lugar | Por quê |
|---|---|---|
| hora em que o turno começou | variável | vale só até o fim do turno |
| arquivos editados nesta sessão | $.state | a tela mostra; é desta sessão |
| faixa ligada ou desligada | $.store + $.state | preferência que a tela também lê |
| um relatório para abrir depois | arquivo, com $.fs.write | é para a pessoa ler fora do Claude (módulo 2.4) |
Na pasta do inema-mods (o comando responde sem chamar o modelo):
claude -p "/recibo" --plugin-dir mods/recibo-sessao
Saída real (05/10/2026):
recibo-sessao: Nesta sessão: nenhum arquivo criado ou alterado.
claude -p é uma sessão nova, e o recibo guarda a lista em $.state, não em $.store. É a escolha certa para um recibo "desta sessão".Teste rápido (opcional): o Rafa quer um botão "esconder" na faixa que continue escondido amanhã. Onde ele guarda esse liga/desliga?
🎓 Resumo do módulo
Próximo módulo:
2.4 — Trabalho fora do dispatch