MÓDULO 5.4

🚧 Portão de execução

Instrução escrita o modelo pode esquecer. Um hook não esquece: se o mentor escreveu código e não rodou, o turno não termina. Neste módulo você entende o script por dentro, vê o que ele aceita como "rodou" e provoca o portão de propósito.

6
Tópicos
50
Minutos
Médio
Nível
Prático
Tipo
0%0 de 6
1

😶 O problema: "parece certo sem estar"

Na trilha 1 você viu o hábito mais caro da IA quando ensina: entregar código com cara de pronto que nunca foi executado. O arquivo do agente já proíbe isso ("dizer 'funciona' sem uma 'Saída real'"). Mas proibição em texto depende de o modelo lembrar e obedecer. O portão de execução é a versão mecânica da mesma regra.

🆕 Novo aqui? Portão

Chamamos de portão um hook que pode dizer "ainda não" ao fim do turno. Tecnicamente, ele imprime {"decision": "block", "reason": "…"} no evento de parada. O Claude Code então não encerra: devolve o reason ao modelo como instrução, e ele continua trabalhando.

Write calc.py "funciona!" → fim do turno tenta parar python3 calc.py PORTÃO decision: block fim ✓ sem portão: nenhuma execução, ninguém percebe "Você escreveu código e não rodou: calc.py"

Como ler: o caminho vermelho de cima é o hábito: escreveu, declarou pronto, acabou. O caminho de baixo é o mesmo agente com o portão: a tentativa de parar volta com o recado, o código roda, e só então o turno termina. O portão não julga se o código está certo; ele exige que alguém o tenha executado, para que a Saída real exista.

Mecanismo

Não depende de obediência

Sem IA

Python stdlib, zero tokens

Exige execução

Não julga o resultado

Nunca derruba

Erro interno = exit 0

2

🧠 Estado por sessão: marca, limpa, bloqueia

Um script só, três papéis, conforme o evento que chega. Entre um evento e outro ele guarda uma lista de arquivos "sujos" (editados e ainda não executados) num arquivo de estado por sessão: .mentor/estado/<session_id>.json, fora do git. Estados com mais de 7 dias são apagados.

.mentor/estado/<sessão>.json {"sujos": [...], "avisados": [...]} PostToolUse · Write/EditMARCA: + arquivo de código PostToolUse · BashLIMPA: − o que o comando executa Stop · SubagentStopBLOQUEIA: sujo não avisado? lê

Como ler: as duas caixas da esquerda escrevem no estado (uma acrescenta, a outra remove); a da direita só lê e decide. Como o estado é por session_id, duas sessões abertas no mesmo projeto não se atrapalham. E o portão não lê o transcript, de propósito: o formato dele pode mudar entre versões do Claude Code.

Configuração (padrão do kit)Valor
O que conta como código.py .js .mjs .cjs .jsx .ts .tsx .sh .go .rs .rb .c .cpp .java
Pastas ignoradasraw wiki docs .claude .mentor
Onde mudarmentor.config.json → "portao"

💡 Por que .claude está na lista de ignoradas

Sem isso, editar o próprio portao-execucao.py ou um hook novo dentro de .claude/ marcaria o arquivo como sujo, e o portão pediria para "rodar" um script que só faz sentido recebendo um evento. Markdown, wiki e acervo também ficam de fora: o portão é sobre código.

3

🚫 O que NÃO conta como rodar

A parte difícil de um portão é não ser enganado. Olhar o arquivo, compilar sem executar ou commitar não provam nada sobre o comportamento. O script quebra cada comando Bash em segmentos (separados por &&, ;, |…) e só limpa o sujo se algum segmento realmente o executa.

✗ Não limpa (o portão continua bloqueando)

  • ls -la · cat calc.py
  • git commit -m "fix calc.py" · git add calc.py
  • rm calc.py · chmod +x calc.py
  • ruff check calc.py
  • python3 -m py_compile calc.py
  • bash -n x.sh
  • echo "calc.py" > log.txt
  • pip install pytest
  • cat calc.py | python3

✓ Limpa (executou de verdade)

  • python3 calc.py --x 2
  • cd pasta && python3 x.py
  • VAR=x python3 a.py
  • python3 -m pacote.tokenizador
  • python3 -m pytest -q (limpa todos os .py)
  • npm test (limpa .js/.ts, inclusive .tsx)

Todos os exemplos acima são casos do arquivo de testes do portão, tests/test_portao.py.

🆕 Novo aqui? py_compile, bash -n e afins

python3 -m py_compile, bash -n, node --check e tsc --noEmit só conferem se o arquivo está bem escrito (a sintaxe). Não executam uma linha da lógica. Um script pode passar em todos eles e ainda dar a resposta errada, por isso o portão os trata como leitura. O mesmo vale para linters como ruff e eslint.

E o cat calc.py | python3?

Esse até executa o código, mas o portão não consegue provar isso só olhando o comando: o primeiro segmento é cat (leitura) e o segundo é python3 sem arquivo. Na dúvida, o portão continua bloqueando. Errar para o lado de "rode de novo, do jeito claro" custa um comando; errar para o outro lado deixa passar código nunca executado.

4

🔂 Anti-loop: no máximo um bloqueio por arquivo editado

O módulo 5.2 mostrou o que acontece com um hook de parada que ignora stop_hook_active: a sessão gira para sempre. O portão do kit nasceu com a defesa: quando o evento de parada chega com stop_hook_active=true, ele não bloqueia. Em vez disso, marca os sujos como "avisados" e sai com exit 0.

1

1º Stop com calc.py sujo → block. O agente recebe o recado.

2

2º Stop, agora com stop_hook_active=true → libera e grava calc.py em avisados.

3

Próximos Stops → não bloqueiam por calc.py de novo, mesmo sem a flag.

4

Editou calc.py de novo? Sai de "avisados" e volta a valer um bloqueio.

Estado gravado depois do 2º Stop, numa simulação feita para esta aula (caminho encurtado):

{"sujos": [".../calc.py"], "avisados": [".../calc.py"]}

O arquivo continua sujo (ninguém o rodou), mas já foi avisado. O próximo Stop dessa sessão termina com exit 0 e nada no stdout.

✓ O que o anti-loop garante

  • ✓A sessão nunca fica presa pelo portão
  • ✓Qualquer erro interno → exit 0: o portão nunca derruba a sessão
  • ✓Stdin que não é JSON → exit 0 (há teste para isso)

✗ O que ele não garante

  • ✗Que o agente rode depois do aviso: ele pode explicar e encerrar
  • ✗Que o código esteja certo: isso é trabalho da Saída real e do validador
5

⚠️ "Stop hook error occurred" e a prova no piloto

Quando o portão bloqueia, a interface do Claude Code mostra o rótulo "Stop hook error occurred". Parece defeito do kit. Não é: é o portão devolvendo o turno com o recado "Você escreveu código e não rodou". O agente roda o código e segue. O piloto anotou essa confusão no diário, e o README do kit 1.2 ganhou uma seção explicando isso.

1

Sessão real, não simulação

No piloto, uma sessão claude -p dentro do projeto do mentor recebeu o pedido de escrever teste_portao.py sem rodar.

2

O Stop foi bloqueado

Com o recado "Você escreveu código e não rodou: teste_portao.py…".

3

O agente explicou e encerrou no 2º Stop

Como o pedido dizia "não rode", o agente explicou a situação e o 2º Stop (já com stop_hook_active) terminou o turno. Um bloqueio, como desenhado. Resultado no placar: "bloqueou certo, sem bloqueio indevido" ✅.

🧪 Testes automáticos do portão

Além da sessão real, o kit tem um arquivo só para o portão. Os casos 1 a 5 cobrem o básico (editou e rodou passa; editou e não rodou bloqueia; só Markdown passa; stop_hook_active passa; SubagentStop também bloqueia). Os outros cobrem as armadilhas do tópico 3.

cd <clone do kit mentor-especialista>
python3 -m pytest tests/test_portao.py -q

Como verificar: a última linha diz 28 passed (medido no kit 1.3). O portão de previsão tem arquivo próprio, tests/test_previsao.py (9 passed), e o kit inteiro tem 81 testes (python3 -m pytest -q).

🔮 O segundo portão: previsão (kit 1.3)

O portão deste módulo olha para depois: escreveu código e não rodou, o Stop bloqueia. O kit 1.3 acrescentou um portão que olha para antes, no mesmo script .claude/hooks/portao-execucao.py, registrado também em PreToolUse com "matcher": "Bash".

  • • Um comando que executa código só passa se o assistente já escreveu Previsão: … no turno. Se não, o hook nega e explica o que escrever.
  • • Ler/listar (cat, ls, grep), os scripts do kit (tools/*.py de stats, validação e coleta) e --version passam direto.
  • • Num subagente, ele lê a transcrição do próprio subagente: <sessão>/subagents/agent-<id>.jsonl.
  • • Desligável em mentor.config.json → "portao": {"previsao": false}.

Foi esse portão que fechou o piloto em 5/5: no teste 2, rodada 4, ele negou a primeira tentativa de execução e o mentor só rodou depois de prever. A história completa está no módulo 6.1.

6

🧪 Provoque o portão de propósito

Só confie no portão depois de vê-lo bloquear. Há dois jeitos: dentro do Claude Code, no projeto do mentor, ou fora dele, entregando ao script os mesmos eventos JSON que o Claude Code entregaria. O segundo leva 30 segundos e não gasta nada.

🅰️ Dentro do Claude Code

Objetivo: ver o bloqueio numa sessão real, como no piloto.

Crie calc.py na raiz do projeto, com uma função que soma dois números e um
print(soma(2, 3)) no final. NÃO rode o arquivo. Depois de criar, encerre.

Como verificar: a interface mostra "Stop hook error occurred" e o agente recebe "Você escreveu código e não rodou: calc.py…". Como você pediu para não rodar, ele deve explicar e encerrar no 2º Stop. Peça de novo sem o "NÃO rode": desta vez ele roda python3 calc.py, mostra 5 e termina sem bloqueio.

🅱️ Fora do Claude Code, só com o terminal

Objetivo: simular os três eventos (editou, tentou parar, rodou) e ler o estado.

mkdir -p aula-portao/.claude/hooks && cd aula-portao
cp <pasta do seu mentor>/.claude/hooks/portao-execucao.py .claude/hooks/
printf 'print(2+3)\n' > calc.py
H="python3 .claude/hooks/portao-execucao.py"; export CLAUDE_PROJECT_DIR=$PWD

echo '{"hook_event_name":"PostToolUse","tool_name":"Write","session_id":"aula","tool_input":{"file_path":"calc.py"}}' | $H
echo '{"hook_event_name":"Stop","session_id":"aula"}' | $H                       # 1) deve bloquear

echo '{"hook_event_name":"PostToolUse","tool_name":"Bash","session_id":"aula","tool_input":{"command":"cat calc.py"}}' | $H
echo '{"hook_event_name":"Stop","session_id":"aula"}' | $H                       # 2) cat não conta: bloqueia

echo '{"hook_event_name":"PostToolUse","tool_name":"Bash","session_id":"aula","tool_input":{"command":"python3 calc.py"}}' | $H
echo '{"hook_event_name":"Stop","session_id":"aula"}' | $H; echo "exit=$?"       # 3) rodou: nada no stdout
cat .mentor/estado/aula.json

Como verificar: as linhas 1 e 2 imprimem o bloqueio (o ê é só o "ê" escapado no JSON):

{"decision": "block", "reason": "Você escreveu código e não rodou: calc.py. Rode cada um (ou os testes) e mostre a saída real antes de dizer que funciona."}

A linha 3 imprime só exit=0, e o estado final é {"sujos": [], "avisados": []}. Essa sequência foi rodada ao preparar esta aula e deu exatamente isso.

⚠️ Se o passo 1 não bloquear

Confira três coisas, nesta ordem: o calc.py está numa pasta ignorada (raw, wiki, docs, .claude, .mentor)? O CLAUDE_PROJECT_DIR aponta para a pasta onde está o calc.py? O session_id é o mesmo nos dois comandos? Qualquer erro interno vira exit 0 silencioso, então um bloqueio que não aparece quase sempre é uma dessas três.

Checagem rápida: o mentor editou calc.py e depois rodou python3 -m py_compile calc.py. O que o portão faz no Stop?

📌 Resumo do Módulo

✓
O problema - "parece certo sem estar"; o portão é a regra virando mecanismo.
✓
Estado por sessão - Write/Edit marca, Bash limpa, Stop/SubagentStop bloqueia.
✓
Não conta - ls, cat, git commit, py_compile, bash -n, linters, cat | python3.
✓
Anti-loop - stop_hook_active libera; no máximo 1 bloqueio por arquivo editado.
✓
"Stop hook error occurred" - é o portão funcionando; provado no piloto e em 28 testes.
✓
Provoque - na sessão real e pelo terminal, antes de confiar.

Próxima Trilha:

6.1 - O piloto real e os testes de aceitação: o que passou, o que não passou e por que instrução não basta.