Objetivo: ao final, o consultor usa exit codes corretamente em hook simples, e nunca mais escreve exit 1 esperando que algo seja bloqueado.
0 = sucesso, o stdout é lido como JSON. 2 = erro bloqueante, o stderr volta para o Claude como contexto. Qualquer outro valor, incluindo 1, não bloqueia. Isso contraria a convenção de shell de propósito.SessionStart, UserPromptSubmit e UserPromptExpansion. Nos outros, vai só para o log de depuração.PostToolUse, porque a ferramenta já rodou. Se a regra precisa impedir, o evento é PreToolUse.| Exit code | Significado | Bloqueia? | O que acontece com a saída |
|---|---|---|---|
0 |
Sucesso | Não, por si só | Stdout é interpretado como JSON (aula 3.2.3); em três eventos, texto puro vira contexto |
2 |
Erro bloqueante | Sim | Stderr volta para o Claude como contexto, para ele se corrigir |
1, 3, qualquer outro |
Erro não bloqueante | Não | Registrado; a execução continua |
Exceção: WorktreeCreate bloqueia com qualquer valor diferente de zero.
exit 1 enganaEm shell, exit 1 é o jeito universal de dizer "falhei". Todo mundo tem isso automatizado no dedo. Em hook do Claude Code, exit 1 significa "houve um erro, mas siga em frente".
O sintoma é traiçoeiro: o hook parece funcionar. Ele detecta o problema, escreve a mensagem, você vê a mensagem na tela — e a ação acontece de qualquer forma. Um hook de política escrito com exit 1 é uma garantia que existe só na aparência.
#!/usr/bin/env bash
# ATENÇÃO: em hook do Claude Code, só exit 2 bloqueia. exit 1 NÃO bloqueia.
if grep -qE 'MRDQAS|MRDPRD' <<< "$CLAUDE_TOOL_INPUT"; then
echo "Regra Wayon: nenhuma ação automatizada em QAS ou PRD do MRD." >&2
exit 2 # <-- 2, nunca 1
fi
exit 0
Vale manter esse comentário no topo de todo hook de bloqueio que a squad escrever. É a linha que evita o erro na revisão.
As duas formas funcionam. A escolha é de expressividade:
| Exit code | JSON no stdout | |
|---|---|---|
| Complexidade | Script de três linhas | Precisa montar e imprimir JSON |
| O que consegue expressar | Bloquear ou não bloquear | allow / deny / ask / defer, razão, e reescrever a chamada |
| Quando usar | Regra binária simples | Quando precisa de razão estruturada, ou de updatedInput |
Para política de ambiente — binária, sem nuance — exit code basta. Para redigir dado pessoal, só JSON serve, porque só ele tem updatedInput.
| Evento | Para que serve |
|---|---|
SessionStart |
Injetar contexto no início da sessão — e reinjetar depois de compactar (aula 3.2.5) |
UserPromptSubmit |
Acrescentar contexto junto do prompt que o consultor acabou de enviar |
UserPromptExpansion |
Acrescentar contexto na expansão de comando |
Em todos os outros eventos, texto puro no stdout vai apenas para o log de depuração. É por isso que a aula 3.2.5 usa SessionStart e não PostCompact: o mecanismo de reinjeção depende deste comportamento.
PostToolUse, PostToolUseFailure, PermissionDenied, Notification, MessageDisplay, SessionStart, SessionEnd, Setup, SubagentStart, StopFailure, InstructionsLoaded, PostCompact, CwdChanged, FileChanged, WorktreeRemove.
A lógica é sempre a mesma: ou a ação já aconteceu (PostToolUse, PermissionDenied), ou o evento é informativo (Notification, MessageDisplay). Em SessionStart, um exit 2 aparece como aviso, não como bloqueio.
Regra Wayon Todo hook de bloqueio da Wayon é revisado por um par antes de entrar em pasta de cliente, e a revisão tem um item obrigatório: confirmar que o caminho de bloqueio usa
exit 2, nãoexit 1. Um hook de política comexit 1passa em teste visual — a mensagem aparece — e falha em produção, porque a ação acontece do mesmo jeito.
exit 1. O que acontece com o comando?Esse é o comportamento do exit 2. Com exit 1, a razão pode até aparecer, mas nada é bloqueado.
exit 1 não bloqueiaCorreto. É a pegadinha central da aula: o hook parece funcionar, e a ação acontece de qualquer forma.
Não há bloqueio nenhum com exit 1; a devolução da razão é característica do exit 2.
0, com a razão no stdoutexit 0 por si só não bloqueia; para negar com exit 0 seria preciso um JSON com permissionDecision: "deny" (aula 3.2.3), não texto solto.
1, com a razão no stderrexit 1 não bloqueia — a ação acontece mesmo com a mensagem aparecendo.
2, com a razão no stderrCorreto. exit 2 bloqueia e o stderr volta para o Claude como contexto.
PostToolUse detecta que a edição feita violou a convenção de nomenclatura e sai com exit 2. O que acontece com a edição?exit 2 bloqueiaexit 2 bloqueia ação futura; não desfaz o que já foi executado — e PostToolUse está na lista de eventos que ignoram bloqueio.
PostToolUse roda depois do sucesso da ferramenta e ignora bloqueio; para impedir seria preciso PreToolUseCorreto. Quando PostToolUse dispara, a edição já existe.
O evento dispara depois da execução bem-sucedida da ferramenta; não há o que impedir nesse ponto.
Stop que imprime, em texto puro no stdout, um resumo do que ficou pendente. O Claude não parece ver esse texto.Não vai sempre: na maioria dos eventos, texto puro no stdout vai apenas para o log de depuração.
exit 2 para que o texto chegue ao ClaudeEm Stop, o caminho de devolver texto é o JSON (decision: "block" com additionalContext), não texto puro somado a exit code.
SessionStart, UserPromptSubmit e UserPromptExpansion — em Stop, é preciso usar JSONCorreto. São exatamente três eventos com esse comportamento.
exit 0 significa falha, ao contrário do shellexit 0 significa sucesso nos dois — nisso a convenção coincide.
2 bloqueiaCorreto — e é justamente a experiência prévia em shell que faz a pessoa escrever exit 1 no automático.
A convenção é estável entre eventos; a única exceção é WorktreeCreate, que bloqueia com qualquer valor diferente de zero.