
Plugins São a Forma de Entregar um Claude Code Harness
Table of Contents
Eu construí um tutor cujo único trabalho era me fazer digitar o código eu mesmo.
Fazer isso funcionar levou quatro tentativas. Cada uma falhou de uma forma que a correção anterior não conseguiu prever e, ao final, eu entendi para que serve realmente um plugin do Claude Code.
Eu queria o tutor porque não queria ser um macaco da web. Anos de CRUD, o que é legal e paga bem, e eu ainda não saberia te dizer o que fork realmente faz. Eu queria as coisas por baixo: processos, sinais, sockets.
Você não consegue isso lendo. Eu sei, porque tentei escrever isso como um livro primeiro. Vinte capítulos, de ch01-what-is-a-web-server.md a ch20-what-we-didnt-build.md, e não teria ensinado nada a ninguém, inclusive a mim, porque em nenhum lugar em vinte arquivos markdown o leitor é forçado a digitar Process.wait e errar. Para aprender código, você tem que colocar as mãos nele.
O que eu não queria era construir uma plataforma para hospedar um tutor. Sem web app, sem contas, sem "ambiente de aprendizagem" para instalar. Isso morre na primeira semana, quando o custo de configuração vence a curiosidade.
Então me ocorreu que o tutor já estava instalado. Minha empresa fornece o Claude Code para cada funcionário: cada máquina, autenticada, já em $PATH. Isso não é apenas uma ferramenta parada ali. Isso é um canal de distribuição.
Então o demonkey é um dojo onde o Claude Code te ensina a construir servidores web em Ruby através da família de processos, desde um socket TCP bruto até um servidor preforking similar ao Unicorn com heartbeats, graceful shutdown e restart de USR2 com zero-downtime. Ele ensina, faz quizzes e revisões. Você digita o código.
TL;DR: Se a sua empresa já distribui o Claude Code, você tem um runtime em cada máquina e enviar um workflow custa um
git clone. Um plugin é o formato de entrega. Ele agrupa as três camadas às quais seu agente responde: aconselhamento (CLAUDE.md), injeção (umadditionalContextde hook), e imposição (um deny dePreToolUse), além de estado em disco para que o fluxo de trabalho sobreviva à compactação e a laptops fechados. O detalhe é que a imposição só atinge coisas que uma chamada de ferramenta expõe, então a última milha são seus dados e suas avaliações. Eu construí isso para um dojo de ensino, mas funciona para qualquer coisa que sua equipe faça da mesma maneira duas vezes.
Aqui está o que foi necessário para mantê-lo longe do teclado.
Tente um: a regra, e a parede que a torna verdadeira
Eu já havia escrito sobre por que a prosa sozinha não sustenta, em Your AI Skills Setup Is Probably Wrong. Versão curta: skills e CLAUDE.md são a mesma substância, markdown que se torna prompt, e a única coisa que os separa é se o prompt chega ou não ao modelo.
Então o demonkey nunca teve uma fase apenas de prosa. A regra e sua imposição foram enviadas no mesmo commit, no primeiro dia. Aqui está a regra, de skills/tutor/SKILL.md:
## The one rule that defines this course
**The learner types the spine. You never write it.** The "spine" is the handful of
lines that *are* the lesson for the current step. You may:
- **explain** docs and APIs (cite the bundle file, never recall from the web),
- **generate glue** - only the files the step explicitly marks as `[glue]`,
- **review** the learner's spine by pointing at the exact line and naming the
problem - *without rewriting it*.
A `PreToolUse` hook will block you from writing the current spine file. That is
intended. If you feel the urge to "just fix it," stop and ask a question instead.Leia aquele último parágrafo novamente. A prosa cita seu próprio mecanismo de aplicação. A instrução não está pedindo cooperação, ela está explicando um muro que o modelo está prestes a atingir. Esse enquadramento importa: uma negação não explicada produz um agente que tenta a mesma coisa de três maneiras diferentes.
O muro em si são quinze linhas de bash, de hooks/guard.sh:
spine)
spine="$("$ROOT/bin/dojo.sh" spine 2>/dev/null || true)"
# Nothing to protect for demo/no-spine steps.
[[ -z "$spine" || "$spine" == "-" || "$spine" == */ ]] && exit 0
fp="$(printf '%s' "$PAYLOAD"
| grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"'
| head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)".*/1/')"
[[ -z "$fp" ]] && exit 0
case "$fp" in
*"$spine")
deny "${spine} is the learner's spine for this step - the lines that ARE the
lesson. Do not write or edit it. Instead: explain the relevant docs, let
the learner type it, then review by pointing at specific lines. (You may
write the glue files named in the step.)"
;;
esac
exit 0
;;E deny é apenas isso:
deny() {
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}n' "$1"
exit 0
}Esse é o mecanismo completo. Imprima JSON com permissionDecision: deny, exit 0, e a chamada da ferramenta não acontece. O modelo não tem a chance de pesar isso contra o fato de ser útil.
Dois detalhes que valem a pena copiar.
O motivo da negação é um prompt.permissionDecisionReason é o único texto que o modelo vê após um bloqueio, então é a sua última chance de direcionar. Note que a mensagem não apenas recusa, ela diz o que fazer em vez disso. Uma negação que diz "proibido" gera tentativas repetidas. Uma negação que diz "explique a documentação, deixe o aluno digitar, depois revise linhas específicas" gera um tutor.
A regra é dinâmica porque lê o estado. O guard pergunta a bin/dojo.sh spine qual é o arquivo da lição de hoje. O caminho protegido muda conforme o aluno avança, a partir das mesmas quinze linhas. Aqui está todo o currículo de onde ele lê, curriculum/steps.tsv:
1 Raw TCP echo server workspace/echo.rb tcp
2 Rack app over a raw socket workspace/rack_server.rb http
3 Why one server is not enough - demo
4 Fork-per-connection workspace/fork_echo.rb tcp
5 Preforking N workers workspace/prefork.rb tcp
6 Master process: signals and reaping workspace/master.rb tcp
7 Production-grade preforking workspace/unicorn_like.rb httpSete linhas, quatro colunas, e três partes diferentes do plugin as leem: o guard utiliza a coluna 3 para saber o que travar, o hook de título utiliza a coluna 2 para nomear a sessão, o comando bench utiliza a coluna 4 para escolher um harness. O Passo 3 tem - para uma spine, então o guard não faz nada nesse dia.
Tudo está conectado em hooks/hooks.json, que carrega automaticamente:
{
"hooks": {
"SessionStart": [
{ "matcher": "startup|resume|clear|compact",
"hooks": [ { "type": "command", "command": ""${CLAUDE_PLUGIN_ROOT}"/hooks/session-start.sh" } ] }
],
"PreToolUse": [
{ "matcher": "WebFetch|WebSearch",
"hooks": [ { "type": "command", "command": ""${CLAUDE_PLUGIN_ROOT}"/hooks/guard.sh web" } ] },
{ "matcher": "Bash",
"hooks": [ { "type": "command", "command": ""${CLAUDE_PLUGIN_ROOT}"/hooks/guard.sh bash" } ] },
{ "matcher": "Write|Edit",
"hooks": [ { "type": "command", "command": ""${CLAUDE_PLUGIN_ROOT}"/hooks/guard.sh spine" } ] }
]
}
}Um detalhe que me custou uma tarde: não adicione um campo hooks ao plugin.json. Hooks carregam automaticamente de hooks/hooks.json, e declarar ambos gera um erro de "Duplicate hooks file". Há um teste forçando isso agora, porque eu quebrei duas vezes.
Tentativa dois: o tutor encontrou a brecha que o hook não conseguiu cobrir
Eu executei o piloto com um aluno real. O hook funcionou perfeitamente. A lição ainda vazou.
O tutor nunca tocou no arquivo da spine. Ele apenas leu o código em voz alta, linha por linha, para o aluno transcrever. A letra foi obedecida, o espírito destruído. Um hook PreToolUse limita chamadas de ferramentas. Ele não pode limitar a prosa.
Pior, quando fui procurar a causa, a instrução que deveria proteger o esforço foi escrita como uma ordem de ditado:
4. **Type the spine** - tell the learner exactly what to type and where (the spine
file, the approximate line count, the primitives to use), and which bundle docs
to read first. Then **wait** for them to write it. Do not write it for them."Diga ao aluno exatamente o que digitar." Eu mesmo escrevi o bug. A correção:
4. **Type the spine** - set them up to WRITE it; do NOT dictate it. Give only: the
file + its rough size, the GOAL (what it must do), the SHAPE at a high level
(e.g. "an accept loop; per connection: parse -> build env -> call app -> write
response -> close in `ensure`"), and which docs to read. Then **wait**. Do NOT
enumerate the lines or hand a transcribe-this checklist - that's copying, not
learning.Mas o prompt não foi o único infrator. Os dados do currículo também eram uma receita. Passo 4, antes:
## Spine (the learner types `workspace/fork_echo.rb`, ~8 lines)
Start from `workspace/echo.rb`. Type the fork block by hand:
- inside the accept loop, `pid = fork do ... end`,
- in the child: close the listening socket, run the echo, then `exit`,
- in the parent: close the accepted connection socket, then reap children so they
don't become zombies - `Process.detach(pid)` *or* a non-blocking
`Process.wait(-1, Process::WNOHANG)` sweep.Depois:
## Spine (the learner types `workspace/fork_echo.rb`, ~8 lines)
Start from `workspace/echo.rb`. The shape (the learner is new to `fork` - teach the
pieces, don't hand the code): for each accepted connection, `fork` a child that
handles it while the parent loops back to `accept`. Two things they must reason
through:
- after `fork`, the child and parent each hold both sockets - which does each keep,
which does it close?
- a finished child becomes a **zombie** until the parent **reaps** it. What is
reaping, and which `Process` call does it? (explain the options + the
non-blocking-vs-blocking trade-off; let them pick and write it).Mesma lição, mesmos detalhes, sem transcrição. A camada de aplicação não conseguia alcançar isso, porque nada aqui é uma chamada de ferramenta. É uma reescrita da coisa que o modelo está lendo.
Tentativa três: o harness é maior que o plugin
A falha seguinte não foi minha. Foi do editor.
A caixa de entrada do Claude Code sugere seu próximo prompt. Em uma sessão normal, isso é útil. Em um dojo, isso entrega ao aluno o movimento que ele deveria derivar. Texto esmaecido que diz "agora adicione Process.detach(pid)" derrota todo o design.
Você não pode bloquear isso com um hook, porque os hooks rodam tarde demais. Você não pode configurar isso a partir da config do plugin, porque o settings.json integrado de um plugin suporta apenas algumas chaves e env não é uma delas. O único lugar que pode desativá-lo é o ambiente do processo, antes de o claude iniciar. Portanto, este fica fora do plugin, em um script de inicialização:
# Turn OFF Claude Code's input-box prompt suggestions for the dojo. The learner is
# meant to think and type the load-bearing code themselves; a grayed-out "type this"
# hand-out breaks that. The flag is read from the process env at startup, so it must
# be set HERE, before claude launches - a plugin's settings.json only supports the
# `agent`/`subagentStatusLine` keys (not `env`), and a hook runs too late to set it.
export CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false
exec claude
--plugin-dir "$PLUGIN_DIR"
"$@"
--disallowed-tools WebSearch WebFetchEsta é a parte que as pessoas esquecem quando dizem "harness." Seu plugin é uma peça disso. O resto são as flags com as quais você inicia, o ambiente em que você inicia e quaisquer partes da ferramenta que você só consegue acessar por fora. --disallowed-tools há redundância: o guard hook já nega o acesso à web, mas a flag da CLI significa que as ferramentas sequer aparecem.
Tentativa quatro: quando você não pode confiar no prompt, teste-o
A última falha foi a mais embaraçosa e a mais útil.
O tutor faz perguntas após cada etapa. Eu tinha escrito os checkpoints como perguntas finalizadas nos arquivos de etapa, com a resposta correta listada primeiro e os distratores depois, marcados com um checkmark organizado:
### Concept check (AskUserQuestion)
**Question:** the first `nc` echoes fine, and a second `nc` opened while the first is
still connected *hangs*. Why?
- ✅ **The process is busy in the read loop for client 1 and never returns to
`accept`.** Confirm, then add: the 2nd connection isn't rejected - the kernel
holds it in the *listen backlog*.
- ❌ "`accept` only returns once." -> Correct them: it's called in a loop.
- ❌ "The OS refuses the second connection." -> No, it's queued in the backlog.O tutor as recitava em ordem. O que significava que a resposta correta era a opção um em cada checkpoint de cada etapa. Um aluno poderia passar no curso inteiro apenas apertando enter.
A correção foi estrutural. Pare de enviar perguntas, envie a matéria bruta da qual o modelo deve compor:
### Checkpoint - why the second client hangs
- **Anchor:** the first `nc` echoes fine; a second `nc` opened while the first is
still connected hangs with no echo.
- **Land this concept:** the process is busy in the read loop for client 1 and never
returns to `accept`. Nuance: the 2nd connection isn't rejected - the kernel holds
it in the *listen backlog* until someone calls `accept` again.
- **Misconception - "`accept` only returns once":** it's called in a loop and
returns a *new* socket each time; the problem is we don't *get back* to it.
- **Misconception - "the OS refuses the second connection":** no - it's queued in
the backlog and served the instant we call `accept` again.
- *Fast-walker transfer:* what if you raised the `listen` backlog to 1000 - does the
second client still hang? (Yes - one process still serves one at a time.)Não há mais ordem para recitar. O tutor tem que construir a pergunta ao vivo e calibrá-la de acordo com a forma como aquele aluno realmente percorreu a etapa.
Então a parte importante. "Eu disse para ele embaralhar" não é evidência, então o commit seguinte criou um eval: inicia o tutor real em modo headless com claude --plugin-dir -p, conduz até o momento do quiz como perfis de alunos roteirizados e captura a chamada de ferramenta AskUserQuestion que ele compõe via --output-format stream-json. Nenhum humano responde a nada; você inspeciona as opções geradas.
No passo 1, a resposta correta ficou em primeiro 0 vezes em 8 em ambos os perfis, com redações distintas, e o perfil rápido recebeu questões de transferência mais difíceis do que o perfil com dificuldades. Cada execução é carimbada com o SHA do git, então duas versões de um prompt podem ser comparadas para ver onde a resposta ficou.
Você pode fazer testes de regressão em um prompt. Essa é a camada acima da aplicação, e quase ninguém a constrói.
Os invariantes também recebem testes unitários. Da suíte do brk, chamando o guard real do demonkey com um payload sintético:
test('spine mode denies writing the current spine file, allows glue files', () => {
const spine = spawnSync('bash', [path.join(PLUGIN_ROOT, 'bin', 'dojo.sh'), 'spine']).stdout.trim();
const writeSpine = JSON.stringify({ tool_input: { file_path: spine } });
assert.ok(isDeny(guard('spine', writeSpine).stdout), 'spine write should be denied');
const writeGlue = JSON.stringify({ tool_input: { file_path: 'workspace/some_glue_helper.rb' } });
assert.equal(guard('spine', writeGlue).stdout.trim(), '', 'glue write should be allowed');
});O que o plugin realmente é
Então: uma regra com uma parede atrás dela, depois os dados que a regra lê, depois o ambiente em que tudo isso roda, depois um eval para provar que ainda se sustenta. Quatro tarefas separadas. Aqui está o que as torna uma entrega única.
demonkey/
├── .claude-plugin/plugin.json # manifest
├── skills/tutor/SKILL.md # the Socratic loop
├── commands/ # /start /next /hint /reveal /setup /status /bench
├── hooks/
│ ├── hooks.json # event -> script
│ ├── guard.sh # the jail
│ ├── session-start.sh # resume + inject the step
│ ├── title.sh # session title tracks the step
│ └── post-bench.sh # scrape bench results into results.csv
├── bin/dojo.sh # state helper (progress.json + steps.tsv)
├── curriculum/ # step-01..07.md, steps.tsv, reference/
└── demonkey.sh # launcher (env + flags)O manifesto tem nove linhas:
{
"name": "demonkey",
"description": "Tutored, constrained build of Ruby web servers through the PROCESS family only...",
"version": "0.1.0",
"author": { "name": "Antonio Barbosa" },
"keywords": ["ruby", "processes", "fork", "preforking", "unicorn", "signals", "sockets"],
"skills": "./skills/",
"commands": ["./commands/"]
}Um desses arquivos faz algo que um CLAUDE.md estruturalmente não consegue: ele roda antes de o aluno digitar qualquer coisa. O session-start.sh inicializa o estado e injeta o passo atual.
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s","sessionTitle":"%s"}}n' "$ctx" "$title"Não existe o "lembre-se de rodar /start". Ele também antecipa a configuração em um projeto novo, o que precisa acontecer enquanto a rede ainda está disponível, antes de a jail fechar:
if [[ "$setup_done" -eq 0 ]]; then
setup_prefix="FIRST ACTION, before ANY tutoring: this project is not set up yet.
Run the /demonkey:setup steps NOW - create workspace/, vendor the pinned gems,
build the offline docs bundle. It is safe and idempotent and must run now while
the network is available (before any offline jailing). === "
fiNote que este é o additionalContext, a camada de injeção, e é a mais fraca das três. O modelo a trata como um <system-reminder> em vez de tratá-la como a sua voz, e pode decidir ignorá-la. Isso não é problema aqui, porque nada essencial depende dela: as regras que devem ser mantidas estão no guard, e o estado está no disco. Use injeção para orientação, nunca para garantias.
Como ele sabe onde você estava três semanas atrás
Esta é a parte que mais me surpreendeu, porque presumi que seria a parte difícil e acabou sendo um arquivo de texto.
O modelo não lembra de nada. Janelas de contexto são compactadas, sessões são /cleared, laptops são fechados. Portanto, o harness mantém sua memória inteiramente fora da conversa, no diretório do projeto:
my-workshop/
├── .demonkey/
│ ├── progress.json # where you are
│ ├── .setup_done # sentinel: deps vendored, docs bundled
│ ├── .titled_step # last step we renamed the session for
│ └── results.csv # benchmark rows, accumulated across sessions
└── workspace/
├── echo.rb # step 1, yours
└── fork_echo.rb # step 4, yoursprogress.json possui quatro campos:
{ "step": 5, "completed": [1, 2, 3, 4], "spine_file": "workspace/prefork.rb", "mode": "local-jailed" }Ele fica na sua pasta do projeto, não em ~/.claude. Isso é deliberado, e é a única decisão que torna tudo isso utilizável:
# State is PER-PROJECT (lives in the learner's project dir), NOT global - so a new
# folder starts fresh at Step 1 instead of inheriting another project's progress.
DATA_DIR="${CLAUDE_PROJECT_DIR:-$PWD}/.demonkey"Uma nova pasta começa no passo 1. Você pode executar o dojo três vezes em três diretórios e eles não sabem uns dos outros. O estado é limitado ao trabalho, da mesma forma que um diretório .git é.
A leitura e a escrita dele são deliberadamente simples. Sem jq, sem dependências, apenas sed e awk:
read_field() { # read_field <key>
sed -n "s/.*"$1"[[:space:]]*:[[:space:]]*"{0,1}([^",}]*)"{0,1}.*/1/p" "$PROGRESS" | head -1
}
tsv_col() { # tsv_col <step> <colnum>
awk -F 't' -v s="$1" -v c="$2" '$1==s{print $c}' "$TSV"
}
advance)
step="$(read_field step)"; comp="$(read_completed)"
if [[ -z "$comp" ]]; then comp="$step"; else comp="$comp, $step"; fi
next=$((step + 1))
spine="$(tsv_col "$next" 3)"; [[ -z "$spine" || "$spine" == "-" ]] && spine="workspace/"
write_progress "$next" "$comp" "$spine" "$(read_field mode)"
echo "Advanced to step $next ($(tsv_col "$next" 2))" ;;Agora veja o que acontece quando você volta depois de três semanas.
Você executa claude nessa pasta.SessionStart dispara antes de você digitar um caractere. session-start.sh lê progress.json, obtém step: 5, busca a linha 5 de steps.tsv para resolver o título e o arquivo do currículo, e emite um objeto JSON com additionalContext dizendo ao modelo: você é o tutor, o aluno está no passo 5, leia curriculum/step-05.md, eles digitam workspace/prefork.rb, não ouse escrever isso. Ele define o título da sessão para demonkey - Step 5: Preforking N workers no mesmo instante.
O guard se reconfigura. Você não disse nada a ele. guard.sh spine chama o shell para dojo.sh spine, que lê o mesmo arquivo e agora nega escritas em workspace/prefork.rb em vez de fork_echo.rb do passo 4. Seu arquivo do passo 4 está desbloqueado novamente, porque não é mais a lição.
A configuração não é executada novamente. O sentinela .setup_done está lá, então o prefixo "execute a configuração primeiro" nunca é injetado e você volta direto para a tutoria.
Você passa o passo e executa /next. O comando é um portão antes de ser uma mudança de estado:
1. Confirm the current step's **success check** passed and the learner passed the
**explain-it-back gate**. If not, do NOT advance - return to the tutor loop.
2. If earned, run `"${CLAUDE_PLUGIN_ROOT}/bin/dojo.sh" advance` to update progress.json.
3. Read the new step's curriculum file and begin its tutor loop.advance aumenta o número, anexa a completed e resolve a próxima espinha. No seu próximo prompt, title.sh percebe que o passo mudou (ele armazena o último em .titled_step, então é uma operação nula em todos os outros prompts) e renomeia a sessão.
Esse é todo o modelo de memória. Um arquivo de texto, uma busca awk e um hook que lê ambos antes de o modelo acordar. Sem banco de dados, nada na janela de contexto. E como é um arquivo simples no seu projeto, você pode dar cat nele quando esquecer onde estava, ou deletá-lo e recomeçar o curso.
Há mais uma peça que vale a pena mostrar, porque é o mesmo truque aplicado a um problema diferente. Os passos 4 e 5 executam um benchmark que causa OOM-kill em um servidor fork-per-connection, depois o executa novamente contra um preforking, e a comparação é a lição. Pedir ao modelo para lembrar de um número entre duas sessões seria bobagem, então um hook PostToolUse extrai isso da saída da ferramenta:
# env/bench/run.sh emits its result as a single marker line:
# PDOJO_RESULT,<server>,<budget>,<held>,<peak_rss_mb>,<oom>
# We grep the tool payload for that marker (robust through JSON escaping because the
# fields are comma-separated bare tokens) and append to results.csv. No-op otherwise.As linhas se acumulam em .demonkey/results.csv por conta própria. Quando o passo 5 quer comparar com o passo 4, o número é uma leitura de arquivo, não uma memória.
Isso não é sobre ensinar Ruby
Eu construí isso para um dojo, mas nada na maquinaria é educacional. Remova o currículo e o que sobra é um formato geral: um fluxo de trabalho que sua equipe já realiza informalmente, codificado para que o agente o realize da mesma maneira todas as vezes.
Considere o caminho desde um ticket até o código mergeado. A maioria das equipes tem um, a maior parte dele vive na cabeça das pessoas, e cada nova contratação o aprende errando no review. Escrito como um plugin, ele ficaria assim:
- Uma habilidade por fase.
/ticket:startpuxa a issue, reafirma os critérios de aceitação e escreve um plano./ticket:prgera a descrição no seu template, com o ID do ticket no nome da branch porque seu CI precisa dele lá. - Um hook de
SessionStartque injeta a branch atual, o ticket ao qual ela se mapeia e se a working tree está suja. Orientação, não lei. - Um hook de
PreToolUseque negagit commitemmain, ou bloqueia edições emdb/schema.rbquando uma migração não foi gerada, ou se recusa a tocar em um diretório vendored. As regras que atualmente são uma frase no seuCONTRIBUTING.mdque todo mundo ignorou ao rolar a página. - Um hook de
PostToolUseque executa o formatador após cada escrita, para que o diff nunca seja ruidoso. - Estado no disco pelo mesmo motivo que o demonkey o tem. Para qual ticket é esta branch, em qual rodada de review estamos, o que o revisor já sinalizou. Ainda estará lá amanhã.
- Um servidor MCP para Jira ou Linear, enviado no mesmo plugin, para que ninguém o configure manualmente.
Isso é um diretório, um git clone, um número de versão. O onboarding de como sua equipe trabalha se torna uma instalação em vez de uma página de wiki.
O dojo acabou de tornar os requisitos excepcionalmente rigorosos. Um tutor que escreve seu código é inútil de uma forma que uma mensagem de commit levemente errada não é, então cada lacuna apareceu imediatamente. Seu workflow falhará de forma mais silenciosa, o que é pior.
Por que isso importa se sua empresa já usa o Claude Code
Escrever esse plugin é a metade fácil. Colocá-lo em outros doze laptops é onde a maioria das ferramentas internas morre silenciosamente.
Um plugin é um diretório com um manifesto. Para publicar um catálogo deles, você coloca um .claude-plugin/marketplace.json em qualquer repositório git:
{
"name": "brk",
"owner": { "name": "Antonio Barbosa" },
"plugins": [
{ "name": "demonkey", "source": "./dojos/demonkey", "category": "workshop" },
{ "name": "loopcraft", "source": "./dojos/loopcraft", "category": "workshop" }
]
}Esse arquivo é todo o registro. Qualquer pessoa pode então adicioná-lo e instalar a partir dele:
/plugin marketplace add geeksilva97/brk
/plugin install demonkey@brkAbreviação do GitHub, qualquer URL de git, GitLab self-hosted, um caminho local ou uma URL simples para um arquivo JSON. Sem serviço de registro, sem etapa de publicação, sem conta. Seu repositório privado existente é um marketplace.brk install é um wrapper simples sobre exatamente esses dois comandos.
Também existe uma maneira de habilitar plugins para toda a equipe commitando extraKnownMarketplaces e enabledPlugins no .claude/settings.json do projeto, para que os colegas sejam solicitados a instalar no primeiro carregamento. Eu não percorri esse caminho eu mesmo, então vou indicar a documentação em vez de entregar uma configuração que nunca enviei. Uma coisa que vale saber se você for por aí: enabledPlugins é um objeto indexado por name@marketplace, não uma lista. Instale um plugin manualmente e leia o que o Claude Code escreve no seu próprio ~/.claude/settings.json.
Para qualquer coisa efêmera, --plugin-dir pula a instalação completamente:
claude --plugin-dir ./demonkeyRepetível, aceita arquivos .zip e tem um irmão --plugin-url que busca um via HTTP. Durante o desenvolvimento, é todo o ciclo de feedback: edite guard.sh, reinicie, veja se o deny dispara. Um plugin --plugin-dir local também substitui um instalado com o mesmo nome, para que você possa testar uma alteração contra a versão real.
Para um workshop, isso significa que trinta pessoas executam um comando, a configuração global de ninguém é tocada e, quando termina, elas fecham o terminal e tudo desaparece.
Nada disso envolveu um servidor, um cadastro ou pedir para alguém instalar um runtime, porque o runtime já estava lá. Essa é a parte a que eu continuo voltando. O problema difícil em ferramentas internas nunca foi escrevê-las.
Um dojo, depois um registro
demonkey foi um piloto para uma palestra na RubyConf. Assim que o padrão se manteve, ele se generalizou: brk agora carrega seis dojos mais um gerador, dojo-forge, que cria a estrutura de um novo a partir de um tópico e termina com um gate de validação executando claude plugin validate, bash -n em cada script, e um grep para marcadores {{PLACEHOLDER}} remanescentes.
A forge envia uma lista numerada de invariantes que todo dojo gerado deve satisfazer. São apenas meus erros acumulados, escritos onde são verificados em vez de lembrados. "Nenhum campo hooks no manifesto" é a invariante 1 porque eu a quebrei duas vezes.
Uma decisão que eu repetiria. O gerador foi deliberadamente deixado de lado enquanto eu construía os primeiros dojos à mão, com uma nota no commit: "extraia um framework de dojos validados, não o construa especulativamente." Eu construí a coisa duas vezes antes de abstraí-la, e a abstração ficou melhor por causa disso.
Faça a resposta ser não
O último post terminou com "habilidades não estão quebradas, elas apenas não servem para diretrizes". Vou estender isso.
As diretrizes também não estão quebradas. Elas apenas não são a aplicação. Se uma regra é importante o suficiente para você reverter um PR por causa dela, ela não pertence a um arquivo markdown onde um modelo a pondera em relação a ser útil. Ela pertence a um hook. E o hook pertence a um plugin, porque é isso que transforma uma pasta de suas preferências pessoais em algo que toda a sua equipe executa.
O README do demonkey diz isso em quatro palavras, e eu não percebi que era a tese até me sentar para escrever isto: hooks are the jail.
Meu tutor ainda quer escrever meu código. Em cada sessão, eu travo no fork, e ele tenta acessar o arquivo. Ele simplesmente não consegue mais acessá-lo, então ele faz aquilo para o qual eu realmente o construí. Ele ensina.
Pare de pedir gentilmente ao seu agente. Construa a coisa que faz a resposta ser não.
Obrigado por ler!
Referências
- demonkey – o dojo sobre o qual este post trata, com seus harnesses de eval e TUI-test
- brk – seis dojos, a CLI e a forge
- Plugins reference e Hooks reference – o esquema do manifesto e a lista completa de eventos de hook (existem cerca de trinta; este post usa quatro)
- Your AI Skills Setup Is Probably Wrong – os evals por trás de "prose alone doesn’t hold"
We want to work with you. Check out our Services page!



