Voltar para as novidades

Como fazer um hook do Claude Code falhar fechado?

Um hook PreToolUse do Claude Code falha fechado quando um invólucro transforma todo código de saída que não seja 0 nem 2 em 2, e esse invólucro precisa de relógio próprio para sobreviver a um guarda que trava. Medimos 9 braços de 3 execuções cada, 27 sessões no total, no Claude Code 2.1.240 em 22 de agosto de 2026: o guarda sadio barrou a escrita em 3 de 3, o guarda que quebrou antes de decidir liberou em 3 de 3, o invólucro em volta desse mesmo guarda quebrado barrou em 3 de 3, e um guarda que dormiu além do timeout do hook liberou a escrita em 3 de 3 tanto sozinho quanto sob o invólucro simples. Só o invólucro que cronometrou o guarda barrou as 3. O veredito em toda execução foi o arquivo no disco, não o que o agente disse.

O que significa um hook do Claude Code falhar fechado?

Um hook PreToolUse do Claude Code falha fechado quando qualquer coisa que dê errado dentro do guarda produz uma recusa em vez de uma aprovação. O hook é um comando que o Claude Code roda antes de uma chamada de ferramenta que case com o matcher, entregando a ele um payload JSON pela entrada padrão. Código de saída 2 significa negar, e o motivo escrito na saída de erro volta para o modelo. Código 0 significa que o hook não tem objeção. Todo o resto, uma quebra, um arquivo ausente, um erro de sintaxe, um travamento, é o caso interessante, porque o guarda não decidiu nada e a chamada de ferramenta ainda precisa ir para um lado ou para o outro.

Por padrão ela passa. Foi esse o comportamento que medimos em 16 de agosto de 2026 e medimos de novo nesta bateria: um guarda que lê o payload, imprime um erro e sai com 1 não impede a escrita. Falhar fechado, portanto, não é algo que se configura, é algo que se constrói. A receita abaixo tem duas peças, e só encontramos a segunda porque fomos procurar o modo de falha que a nossa própria receita de agosto não cobria.

As palavras aqui importam porque são emprestadas. Falhar fechado e falhar aberto vêm do controle de acesso e dos admission webhooks do Kubernetes, onde a mesma escolha é um campo chamado failurePolicy. Um hook que falha fechado recusa trabalho quando o guarda está quebrado. Um hook que falha aberto deixa o trabalho seguir quando o guarda está quebrado. Nenhum dos dois é certo no abstrato, o que é assunto de uma seção mais adiante.

O que acontece quando um hook do Claude Code quebra antes de decidir?

Um hook do Claude Code que quebra antes de decidir libera o agente, em 3 de 3 execuções, e o agente ainda relata sucesso sem mencionar o hook. O nosso braço quebrado tinha quatro linhas: ler o payload, anexá-lo a um arquivo de rastro, imprimir guard failed before deciding na saída de erro e sair com 1. A tarefa dada a cada sessão foi idêntica, criar notes.txt com a palavra ready no diretório atual e depois confirmar se o arquivo existe. No braço quebrado o arquivo existia nas três execuções, com 6 bytes, e a sessão respondeu com uma confirmação organizada do caminho e do conteúdo.

Esta é a linha de base que a nossa medição de 16 de agosto de 2026 estabeleceu, e a repetimos aqui só para que os braços novos tenham um controle ao lado, na mesma máquina e na mesma versão. O arquivo de rastro é o que impede um arquivo de configuração mal escrito de se passar por resultado: o guarda quebrado registrou 3 payloads nas três execuções, todos de chamadas Bash, ou seja ele foi invocado, viu a escrita chegando e não a impediu. O guarda sadio ao lado dele registrou 6 payloads nas três execuções, 3 de Bash e 3 de Write, porque uma sessão barrada tenta a outra ferramenta antes de desistir.

Essas contagens de interceptação são os números mais moles desta página. Elas contam chamadas de ferramenta que o modelo escolheu fazer, se mexeram entre as nossas próprias execuções, e a única afirmação que sustentam é que o hook foi invocado. O arquivo no disco é o número duro.

Como se envolve um hook para que toda saída inesperada vire recusa?

Você faz um hook do Claude Code falhar fechado apontando o hook para um invólucro em vez de apontar para o seu guarda, e fazendo o invólucro traduzir qualquer código de saída que não seja 0 nem 2 em 2. O invólucro lê o payload uma vez, canaliza para o guarda de verdade, preserva a saída do próprio guarda e repassa 0 e 2 intactos, de modo que aprovações e recusas normais se comportam exatamente como antes. Todo o resto vira uma recusa com um motivo que diz que o guarda não conseguiu decidir.

#!/usr/bin/env bash
# involucro: falhar fechado em volta de qualquer guarda
input="$(cat)"
tmp="$(mktemp)"
printf '%s' "$input" | "$GUARD" > "$tmp" 2>&1
rc=$?
out="$(cat "$tmp")"; rm -f "$tmp"
if [ "$rc" = 0 ] || [ "$rc" = 2 ]; then
  printf '%s\n' "$out" >&2
  exit "$rc"
fi
printf 'guard could not decide (exit %s), refusing by default\n' "$rc" >&2
exit 2

Na nossa bateria o invólucro em volta do guarda quebrado barrou a escrita em 3 de 3 execuções, contra 0 de 3 do mesmo guarda sozinho. As sessões do braço envolvido leram a recusa, tentaram também a ferramenta Write, receberam a mesma resposta, conferiram com Read e relataram que o arquivo não existia. As três citaram a mensagem do invólucro de volta para nós na íntegra, guard could not decide (exit 1), refusing by default, que é o argumento prático para pôr o código de saída no texto: quem estiver depurando aquilo às duas da manhã é informado de qual metade quebrou.

O invólucro ainda funciona quando o script do hook não existe?

O invólucro barra em 3 de 3 execuções mesmo quando o guarda que ele chama não existe no disco, que é o erro de configuração comum e não um caso exótico. Montamos um braço cujo invólucro chamava um caminho que nunca foi criado. O shell devolve 127 para um comando que não encontra, 127 não é 0 nem 2, então o invólucro recusou. Nas três execuções o notes.txt ficou ausente, e uma das sessões diagnosticou a corrente inteira sozinha, nomeando o script ausente, citando o código 127 e chamando o invólucro de fail-closed antes de decidir não procurar um jeito de contorná-lo.

Este braço importa mais do que parece, porque mostra que o invólucro cobre uma classe e não um caso. Um caminho de hook que ficou errado depois de mover o repositório, um guarda que perdeu o bit de execução, um interpretador que não está instalado na máquina do colega, um script que morre numa variável não definida: todos chegam ao invólucro como algum código de saída que não é 0 nem 2, e todos saem de lá como recusa. Você não precisa enumerar os jeitos pelos quais o seu guarda pode quebrar.

Uma limitação honesta deste braço: como o guarda nunca rodou, nada foi anexado ao arquivo de rastro, então a contagem de invocações dele é zero e não corrobora nada. A evidência aqui é o arquivo ausente somado à mensagem de recusa que a sessão citou, e essa mensagem só pode ter vindo do invólucro.

A forma JSON de decisão protege um hook que quebra?

A forma de saída JSON estruturada não protege um hook do Claude Code que quebra, e na nossa bateria ela falhou exatamente como a forma por código de saída, 0 de 3 barrados. O Claude Code permite que um hook saia com 0 e imprima um objeto JSON com um bloco hookSpecificOutput carregando permissionDecision, o que soa mais deliberado do que códigos de saída e costuma ser tomado como mais seguro. Rodamos as duas metades. Um guarda que imprimiu um objeto de negação bem formado e saiu com 0 barrou a escrita em 3 de 3 execuções, ou seja a forma em si funciona e temos o direito de falar dela. Um guarda que quebrou antes de imprimir qualquer coisa liberou a escrita em 3 de 3.

Essa simetria é o ponto. A decisão que para um agente é carregada por algo que o guarda produz no fim da execução dele, seja esse algo um código de saída ou uma linha de JSON, então qualquer falha antes do fim não produz decisão nenhuma. Escolher a forma JSON compra para você uma string de motivo e uma distinção entre negar e perguntar, e no único modo de falha que medimos, um guarda que morre antes de imprimir, não comprou nada. O invólucro é o que compra o caso de falha, e ele envolve um guarda JSON com a mesma facilidade com que envolve um guarda por código de saída, porque um guarda JSON que quebra também sai com algo que não é 0 nem 2.

O que acontece quando um hook do Claude Code trava em vez de quebrar?

Um hook do Claude Code que trava libera a escrita, 3 de 3, quando o timeout do harness estoura. Este é o modo de falha que não tínhamos testado em agosto, e ele se comporta de um jeito diferente do da quebra numa coisa que importa: nada no guarda está defeituoso. O nosso guarda travado era o guarda sadio com um sleep 60 inserido antes da recusa, e a entrada do hook no arquivo de configuração levava "timeout": 5. Todas as execuções criaram o arquivo e o confirmaram, 6 bytes, conteúdo ready, sem menção nenhuma a hook.

Travar é a versão realista desta falha, não a artificial. Um guarda que consulta um servidor de política por HTTP, que lê um arquivo de trava que outro processo está segurando, que chama um scanner externo ou que espera num sistema de arquivos de rede pode demorar mais que o seu timeout estando perfeitamente correto. O rastro mostra que o guarda foi invocado nas três execuções e simplesmente nunca chegou a dizer não.

O que este braço estabelece é estreito: um guarda, dormindo 60 segundos sob um timeout de 5, falhou aberto em 3 de 3 execuções. Ele não estabelece com que frequência uma dependência real estoura o próprio timeout, o que depende do seu servidor de política e da sua rede, não do Claude Code. A consequência de projeto é a parte que viaja: um código de saída dá para traduzir, enquanto uma decisão que nunca chega não pode ser traduzida por quem está esperando por ela, que foi o que a próxima seção mediu.

Por que o invólucro de uma peça só falha contra um hook que trava?

O invólucro sozinho não salva um guarda travado, 0 de 3 barrados, porque o invólucro está esperando pelo guarda quando o harness mata os dois. Previmos isso antes de rodar e a previsão se confirmou: o invólucro só consegue traduzir um código de saída depois que o guarda devolve um, então um guarda que nunca devolve deixa o invólucro sem nada para traduzir. O timeout do harness dispara, o hook não produz decisão, e a escrita passa exatamente como se o invólucro não estivesse ali.

O conserto é uma segunda peça: o invólucro carrega o próprio relógio, mais curto que o timeout do hook, e recusa quando o guarda perde a hora. Na nossa bateria, um invólucro que deu 3 segundos ao guarda e transformou a morte resultante em código 2 barrou a escrita em 3 de 3 execuções contra o mesmo guarda travado. Duas das três sessões citaram a mensagem do invólucro na íntegra, guard could not decide (exit 142), refusing by default, e uma delas observou que o Read continuava funcionando enquanto Bash e Write não.

#!/usr/bin/env bash
# involucro com relogio proprio, 3 segundos, sob um timeout de hook de 5
input="$(cat)"
tmp="$(mktemp)"
printf '%s' "$input" | perl -e 'alarm shift; exec @ARGV' 3 "$GUARD" > "$tmp" 2>&1
rc=$?
out="$(cat "$tmp")"; rm -f "$tmp"
if [ "$rc" = 0 ] || [ "$rc" = 2 ]; then printf '%s\n' "$out" >&2; exit "$rc"; fi
printf 'guard could not decide (exit %s), refusing by default\n' "$rc" >&2
exit 2

Escrever a saída do guarda num arquivo temporário em vez de capturá-la numa substituição de comando não é escolha de estilo, é a diferença entre um invólucro que devolve e um que não devolve. A nossa primeira versão capturava o guarda com $(...), e matar o guarda não fechava o cano, porque o sleep órfão continuava segurando a ponta de escrita aberta, então o invólucro ficava ali lendo de um processo morto. Medidos lado a lado duas vezes em 22 de agosto de 2026, a versão com substituição de comando nunca devolveu sozinha e precisou ser morta por um alarme de fora marcado em 40 segundos, que a pegou aos 40,2 e aos 40,1 segundos, enquanto a versão com arquivo temporário devolveu código 2 em 3,2 segundos nas duas. Descobrimos isso provando o instrumento antes da bateria, não pelos resultados, o que é o argumento para testar o seu guarda à mão com um payload de verdade antes de gastar sessões com ele.

Quando um guarda deveria falhar aberto?

Um guarda deveria falhar aberto quando o custo de recusar é maior que o custo de deixar uma operação passar, e esse caso é real o bastante para um fornecedor publicá-lo como decisão de projeto. Em 20 de agosto de 2026, a PandoCore publicou um texto de Eliot Ferstl intitulado Why We Ship Our Security Webhook Fail-Open, explicando que o admission webhook deles para Kubernetes sai de fábrica com failurePolicy: Ignore, de propósito. O raciocínio: um webhook no caminho crítico da criação de pod que falha fechado não parece um incidente de segurança, parece o cluster quebrando, com ReplicaSets soltando FailedCreate, deploys pendurados e o autoscaler travado.

O mesmo texto é igualmente claro sobre o preço, chamando uma carga de trabalho silenciosamente desprotegida de a pior falha que existe para um produto de segurança, e descrevendo a engenharia de verdade como tornar a falha silenciosa barulhenta, por eventos de aviso e um contador do Prometheus. Ele ainda declara uma lacuna própria, a de que o alerta ligado por padrão para o webhook fora do ar ainda não foi entregue. É um fornecedor argumentando contra a nossa receita com os números dele, e vale a leitura antes de adotar qualquer uma das duas posições como regra.

O que separa as duas situações é o raio de dano, não o princípio. Quando um hook PreToolUse falha fechado, uma sessão de programação para e uma pessoa lê uma mensagem de recusa que nomeia o guarda. Quando um admission webhook falha fechado, um cluster inteiro para de agendar. No CanvasCode rodamos vários agentes de programação lado a lado, que é justamente onde uma barreira que parou de funcionar em silêncio custa mais caro, porque a escrita que você não esperava vem de uma sessão que você não estava olhando. Desse lado da conta, uma sessão travada é barata.

Como reproduzir estes resultados de hook do Claude Code na sua própria máquina?

Este script reproduz os três braços decisivos na sua própria máquina, e você não deveria aceitar os nossos números sem ele. Ele monta três diretórios descartáveis sob mktemp, escreve um guarda que trava e os dois invólucros, dá a mesma tarefa a cada sessão e imprime quais braços terminaram com o arquivo. Ele roda cada braço uma vez em vez de três, então trate o posicionamento como a parte reproduzível, não a contagem. Ele não apaga nada além do arquivo temporário que o próprio invólucro cria, então os diretórios de trabalho e as saídas das sessões ficam no disco para você ler e remover você mesmo.

#!/usr/bin/env bash
# Reproduz os bracos do guarda que trava: pelado, involucro simples, involucro com relogio.
# Roda tres sessoes do Claude Code. Preserva os diretorios de trabalho e as saidas.
set -u
W="$(mktemp -d)" || exit 1
echo "workdir: $W"
cat > "$W/hang.sh" <<'G'
#!/usr/bin/env bash
cat > /dev/null
sleep 60
printf 'this session may not run shell commands\n' >&2
exit 2
G
cat > "$W/plain.sh" <<P
#!/usr/bin/env bash
input="\$(cat)"
out="\$(printf '%s' "\$input" | "$W/hang.sh" 2>&1)"; rc=\$?
if [ "\$rc" = 0 ] || [ "\$rc" = 2 ]; then printf '%s\n' "\$out" >&2; exit "\$rc"; fi
printf 'guard could not decide (exit %s), refusing by default\n' "\$rc" >&2
exit 2
P
cat > "$W/clock.sh" <<C
#!/usr/bin/env bash
input="\$(cat)"
tmp="\$(mktemp)"
printf '%s' "\$input" | perl -e 'alarm shift; exec @ARGV' 3 "$W/hang.sh" > "\$tmp" 2>&1
rc=\$?
out="\$(cat "\$tmp")"; rm -f "\$tmp"
if [ "\$rc" = 0 ] || [ "\$rc" = 2 ]; then printf '%s\n' "\$out" >&2; exit "\$rc"; fi
printf 'guard could not decide (exit %s), refusing by default\n' "\$rc" >&2
exit 2
C
chmod +x "$W"/hang.sh "$W"/plain.sh "$W"/clock.sh
for a in hang plain clock; do
  printf '{"hooks":{"PreToolUse":[{"matcher":"Bash|Write|Edit","hooks":[{"type":"command","command":"%s/%s.sh","timeout":5}]}]}}\n' "$W" "$a" > "$W/$a.json"
  mkdir -p "$W/run-$a"
done
ASK='Create a file named notes.txt in the current directory containing the single word ready. Then confirm whether the file exists.'
for a in hang plain clock; do
  ( cd "$W/run-$a" && claude -p "$ASK" --permission-mode bypassPermissions \
      --settings "$W/$a.json" > "$W/out-$a.txt" 2>&1 < /dev/null ) &
done
wait
for a in hang plain clock; do
  if [ -f "$W/run-$a/notes.txt" ]; then printf '%s: WROTE (failed open)\n' "$a"; else printf '%s: blocked\n' "$a"; fi
done

Dois avisos antes de rodar, e os dois são sobre custo, não sobre perigo. O script chama claude -p três vezes em paralelo com as permissões contornadas dentro de diretórios descartáveis, o que custa o que custam três sessões curtas no seu plano, e o guarda aqui recusa toda chamada que case com o matcher em vez de inspecionar o payload, o que é deliberado para uma demonstração e inútil como política.

Quais comportamentos de hook do Claude Code não foram medidos?

A amostra é pequena e é nossa. Todo número desta página vem de uma máquina rodando Claude Code 2.1.240 no macOS 26.5.2 com bash 3.2.57, em 22 de agosto de 2026, em diretórios descartáveis vazios, 9 braços de 3 execuções. Contagens de um dígito não separam uma falha rara de uma falha impossível. O que elas conseguem fazer, e fizeram, é pôr os braços que barram e os braços que falham abertos em lados opostos em toda execução, sem nenhum braço se dividindo 2 a 1.

Várias coisas não testamos. Não testamos hooks por HTTP, onde a decisão viaja por uma rede que pode estar fora e onde um invólucro não tem processo nenhum para matar. Não testamos o que acontece quando o guarda grava um estado que outra invocação precisa ler, que é uma falha diferente e com literatura própria. Não testamos hooks no Linux nem no Windows, e o relógio do nosso segundo invólucro usa perl com um alarme porque o timeout não existe no macOS de fábrica. Não testamos nenhum agente além do Claude Code. Não testamos como estes guardas se comportam numa sessão conduzida de forma interativa em vez de com claude -p.

Vale declarar um limite com todas as letras, porque ele restringe a receita inteira. Um hook que falha fechado protege as chamadas de ferramenta que o matcher dele cobre, e o nosso cobria Bash, Write, Edit, MultiEdit e NotebookEdit. Read nunca foi casado, e nos braços barrados as sessões usaram Read à vontade para conferir se o arquivo existia. Um guarda que recusa tudo não é uma política, e a parte sobre a qual esta medição nada diz é se a sua política decide certo quando ela chega a decidir.